From ed2f25ca09a1ccdb14868642377feea407ab3483 Mon Sep 17 00:00:00 2001 From: Patric Stout Date: Fri, 9 Sep 2022 22:26:36 +0200 Subject: [PATCH] feat(client): library header-file and empty implementation --- .gitignore | 1 + CMakeLists.txt | 45 +++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 19 +++++++++++++++++++ README.md | 12 ++++++++++++ example/pubsub/CMakeLists.txt | 18 ++++++++++++++++++ example/pubsub/main.cpp | 33 +++++++++++++++++++++++++++++++++ include/TrueMQTT.h | 175 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Client.cpp | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Log.h | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ truemqtt.pc.in | 12 ++++++++++++ 10 files changed, 535 insertions(+), 0 deletions(-) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 example/pubsub/CMakeLists.txt create mode 100644 example/pubsub/main.cpp create mode 100644 include/TrueMQTT.h create mode 100644 src/Client.cpp create mode 100644 src/Log.h create mode 100644 truemqtt.pc.in diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4029bef --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,45 @@ +# +# Copyright (c) TrueBrain +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +# + +cmake_minimum_required(VERSION 3.16) + +project(truemqtt VERSION 1.0.0 DESCRIPTION "A modern C++ MQTT Client library") + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +set(MIN_LOGGER_LEVEL "INFO" CACHE STRING "Set minimal logger level (TRACE, DEBUG, INFO, WARN, ERROR). No logs below this level will be omitted.") + +include(GNUInstallDirs) + +add_library(${PROJECT_NAME} + src/Client.cpp +) +target_include_directories(${PROJECT_NAME} PUBLIC include PRIVATE src) + +set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION 1 PUBLIC_HEADER include/TrueMQTT.h) +configure_file(truemqtt.pc.in truemqtt.pc @ONLY) + +if(MIN_LOGGER_LEVEL) + if(("${MIN_LOGGER_LEVEL}" STREQUAL "TRACE") OR + ("${MIN_LOGGER_LEVEL}" STREQUAL "DEBUG") OR + ("${MIN_LOGGER_LEVEL}" STREQUAL "INFO") OR + ("${MIN_LOGGER_LEVEL}" STREQUAL "WARNING") OR + ("${MIN_LOGGER_LEVEL}" STREQUAL "ERROR")) + target_compile_definitions(${PROJECT_NAME} PRIVATE MIN_LOGGER_LEVEL=LOGGER_LEVEL_${MIN_LOGGER_LEVEL}) + else() + message(FATAL_ERROR "Unknown value provided for MIN_LOGGER_LEVEL: \"${MIN_LOGGER_LEVEL}\", must be one of TRACE, DEBUG, INFO, WARNING or ERROR") + endif() +endif() + + +target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror) + +install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(FILES ${CMAKE_BINARY_DIR}/truemqtt.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) + +add_subdirectory(example/pubsub) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..78b7444 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 TrueBrain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a96558a --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# TrueMQTT - A modern C++ MQTT Client library + +## Development + +```bash +mkdir build +cd build +make .. -DBUILD_SHARED_LIBS=ON -DMIN_LOGGER_LEVEL=TRACE +make -j$(nproc) + +example/pubsub/truemqtt_pubsub +``` diff --git a/example/pubsub/CMakeLists.txt b/example/pubsub/CMakeLists.txt new file mode 100644 index 0000000..c7e4bf8 --- /dev/null +++ b/example/pubsub/CMakeLists.txt @@ -0,0 +1,18 @@ +# +# Copyright (c) TrueBrain +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +# + +cmake_minimum_required(VERSION 3.16) + +project(truemqtt_pubsub) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include_directories(SYSTEM ${CMAKE_CURRENT_SOURCE_DIR}/../../include) + +add_executable(${PROJECT_NAME} main.cpp) +target_link_libraries(${PROJECT_NAME} truemqtt) diff --git a/example/pubsub/main.cpp b/example/pubsub/main.cpp new file mode 100644 index 0000000..42fdd96 --- /dev/null +++ b/example/pubsub/main.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (c) TrueBrain + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +int main() +{ + // Create a connection to the local broker. + TrueMQTT::Client client("localhost", 1883, "test"); + + client.setLogger(TrueMQTT::Client::LogLevel::TRACE, [](TrueMQTT::Client::LogLevel level, std::string message) { + std::cout << "Log " << level << ": " << message << std::endl; + }); + + client.connect(); + + // Subscribe to the topic we will be publishing under in a bit. + client.subscribe("test", [](const std::string &topic, const std::string &payload) { + std::cout << "Received message on topic " << topic << ": " << payload << std::endl; + }); + + // Publish a message on the same topic as we subscribed too. + client.publish("test", "Hello World!", false); + + client.disconnect(); + + return 0; +} diff --git a/include/TrueMQTT.h b/include/TrueMQTT.h new file mode 100644 index 0000000..d2ed2df --- /dev/null +++ b/include/TrueMQTT.h @@ -0,0 +1,175 @@ +/* + * Copyright (c) TrueBrain + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace TrueMQTT +{ + /** + * @brief MQTT Client class. + * This class manages the MQTT connection and provides methods to publish and subscribe to topics. + */ + class Client + { + public: + /** + * @brief Error codes that can be returned in the callback set by \ref setErrorCallback. + */ + enum Error + { + SUBSCRIBE_FAILED, ///< The subscription failed. The topic that failed to subscribe is passed as the second argument. + UNSUBSCRIBE_FAILED, ///< The unsubscription failed. The topic that failed to unsubscribe is passed as the second argument. + DISCONNECTED, ///< The connection was lost. The reason for the disconnection is passed as the second argument. + CONNECTION_FAILED, ///< The connection failed. The reason for the failure is passed as the second argument. + }; + + /** + * @brief The type of queue that can be set for publishing messages. + */ + enum QueueType + { + DROP, ///< Do not queue. + FIFO, ///< Global FIFO. + LIFO, ///< Global LIFO. + LIFO_PER_TOPIC, ///< Per topic LIFO. + }; + + /** + * @brief The log levels used by this library. + */ + enum LogLevel + { + NONE, ///< Do not log anything (default). + ERROR, ///< Something went wrong and the library cannot recover. + WARNING, ///< Something wasn't right, but the library can recover. + INFO, ///< Information that might be useful to know. + DEBUG, ///< Information that might be useful for debugging. + TRACE, ///< Information that is overly verbose to tell exactly what the library is doing. + }; + + /** + * @brief Constructor for the MQTT client. + * + * @param host The hostname of the MQTT broker. Can be either an IP or a domain name. + * @param port Port of the MQTT broker. + * @param client_id Client ID to use when connecting to the broker. + * @param connection_timeout Timeout in seconds for the connection to the broker. + * @param connection_backoff_max Maximum time between backoff attempts in seconds. + * @param keep_alive_interval Interval in seconds between keep-alive messages. + */ + Client(const std::string &host, int port, const std::string &client_id, int connection_timeout = 5, int connection_backoff_max = 30, int keep_alive_interval = 30); + + /** + * @brief Destructor of the MQTT client. + * + * Before destruction, any open connection is closed gracefully. + */ + ~Client(); + + /** + * @brief Set the logger callback and level. + * + * @param log_level The \ref LogLevel to use for logging. + * @param logger The callback to call when a log message is generated. + * + * @note This library doesn't contain a logger, so you need to provide one. + * If this method is not called, no logging will be done. + */ + void setLogger(LogLevel log_level, std::function logger); + + /** + * @brief Set the last will message on the connection. + * + * @param topic The topic to publish the last will message to. + * @param payload The payload of the last will message. + * @param retain Whether to retain the last will message. + */ + void setLastWill(const std::string &topic, const std::string &payload, bool retain); + + /** + * @brief Set the error callback, called when any error occurs. + * @param callback The callback to call when an error occurs. + */ + void setErrorCallback(std::function callback); + + /** + * @brief Set the publish queue to use. + * + * @param queue_type The \ref QueueType to use for the publish queue. + * @param size The size of the queue. If the queue is full, the type of queue defines what happens. + */ + void setPublishQueue(QueueType queue_type, int size); + + /** + * @brief Connect to the broker. + * + * After calling this function, the library will try a connection to the broker. + * If the connection fails, it will try again after a backoff period. + * The backoff period will increase until it reaches the maximum backoff period. + * + * If the connection succeeds, but it disconnected later (without calling \ref disconnect), + * the library will try to reconnect. + * + * @note Calling connect twice has no effect. + */ + void connect(); + + /** + * @brief Disconnect from the broker. + * + * This function will disconnect from the broker and stop trying to reconnect. + * Additionally, it will clean any publish / subscribe information it has. + * + * @note Calling disconnect twice has no effect. + */ + void disconnect(); + + /** + * @brief Publish a payload on a topic. + * + * @param topic The topic to publish the payload on. + * @param payload The payload to publish. + * @param retain Whether to retain the message on the broker. + * + * @note All messages are always published under QoS 0, and this library supports no + * other QoS level. + * @note This call is non-blocking, and it is not possible to know whether the message + * was actually published or not. + */ + void publish(const std::string &topic, const std::string &payload, bool retain); + + /** + * @brief Subscribe to a topic, and call the callback function when a message arrives. + * + * @param topic The topic to subscribe to. + * @param callback The callback to call when a message arrives on this topic. + * + * @note Subscription can overlap, but you cannot subscribe on the exact same topic twice. + * If you do, the callback of the first subscription will be overwritten. + * In other words, "a/+" and "a/b" is fine, and callbacks for both subscribes will be + * called when something is published on "a/b". + */ + void subscribe(const std::string &topic, std::function callback); + + /** + * @brief Unsubscribe from a topic. + * + * @param topic The topic to unsubscribe from. + * + * @note If you unsubscribe from a topic you were not subscribed too, nothing happens. + */ + void unsubscribe(const std::string &topic); + + private: + // Private implementation + class Impl; + std::unique_ptr m_impl; + }; +} diff --git a/src/Client.cpp b/src/Client.cpp new file mode 100644 index 0000000..64de5af --- /dev/null +++ b/src/Client.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (c) TrueBrain + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "TrueMQTT.h" +#include "Log.h" + +#include + +using TrueMQTT::Client; + +// This class tracks all internal variables of the client. This way the header +// doesn't need to include the internal implementation of the Client. +class Client::Impl +{ +public: + Impl(const std::string &host, int port, const std::string &client_id, int connection_timeout, int connection_backoff_max, int keep_alive_interval) + : host(host), + port(port), + client_id(client_id), + connection_timeout(connection_timeout), + connection_backoff_max(connection_backoff_max), + keep_alive_interval(keep_alive_interval) + { + } + + enum State + { + DISCONNECTED, + CONNECTING, + CONNECTED, + }; + + State state = State::DISCONNECTED; ///< The current state of the client. + + std::string host; ///< Host of the broker. + int port; ///< Port of the broker. + std::string client_id; ///< Client ID to use when connecting to the broker. + int connection_timeout; ///< Timeout in seconds for the connection to the broker. + int connection_backoff_max; ///< Maximum time between backoff attempts in seconds. + int keep_alive_interval; ///< Interval in seconds between keep-alive messages. + + Client::LogLevel log_level = Client::LogLevel::NONE; ///< The log level to use. + std::function logger = [](Client::LogLevel, std::string) {}; ///< Logger callback. + + std::string last_will_topic = ""; ///< Topic to publish the last will message to. + std::string last_will_payload = ""; ///< Payload of the last will message. + bool last_will_retain = false; ///< Whether to retain the last will message. + + std::function error_callback = [](Error, std::string &) {}; ///< Error callback. + + Client::QueueType publish_queue_type = Client::QueueType::DROP; ///< The type of queue to use for the publish queue. + int publish_queue_size = -1; ///< Size of the publish queue. +}; + +Client::Client(const std::string &host, int port, const std::string &client_id, int connection_timeout, int connection_backoff_max, int keep_alive_interval) +{ + this->m_impl = std::make_unique(host, port, client_id, connection_timeout, connection_backoff_max, keep_alive_interval); + + LOG_TRACE("Constructor of client called"); +} + +Client::~Client() +{ + LOG_TRACE("Destructor of client called"); + + this->disconnect(); +} + +void Client::setLogger(Client::LogLevel log_level, std::function logger) +{ + LOG_TRACE("Setting logger to log level " + std::to_string(log_level)); + + this->m_impl->log_level = log_level; + this->m_impl->logger = logger; + + LOG_DEBUG("Log level now on " + std::to_string(this->m_impl->log_level)); +} + +void Client::setLastWill(const std::string &topic, const std::string &payload, bool retain) +{ + LOG_TRACE("Setting last will to topic " + topic + " with payload " + payload + " and retain " + std::to_string(retain)); + + this->m_impl->last_will_topic = topic; + this->m_impl->last_will_payload = payload; + this->m_impl->last_will_retain = retain; +} + +void Client::setErrorCallback(std::function callback) +{ + LOG_TRACE("Setting error callback"); + + this->m_impl->error_callback = callback; +} + +void Client::setPublishQueue(Client::QueueType queue_type, int size) +{ + LOG_TRACE("Setting publish queue to type " + std::to_string(queue_type) + " and size " + std::to_string(size)); + + this->m_impl->publish_queue_type = queue_type; + this->m_impl->publish_queue_size = size; +} + +void Client::connect() +{ + if (this->m_impl->state != Client::Impl::State::DISCONNECTED) + { + return; + } + + LOG_INFO("Connecting to " + this->m_impl->host + ":" + std::to_string(this->m_impl->port)); + + this->m_impl->state = Client::Impl::State::CONNECTING; +} + +void Client::disconnect() +{ + if (this->m_impl->state == Client::Impl::State::DISCONNECTED) + { + LOG_TRACE("Already disconnected"); + return; + } + + LOG_INFO("Disconnecting from broker"); + + this->m_impl->state = Client::Impl::State::DISCONNECTED; +} + +void Client::publish(const std::string &topic, const std::string &payload, bool retain) +{ + LOG_DEBUG("Publishing message on topic '" + topic + "': " + payload + " (" + (retain ? "retained" : "not retained") + ")"); +} + +void Client::subscribe(const std::string &topic, std::function callback) +{ + LOG_DEBUG("Subscribing to topic '" + topic + "'"); + + (void)callback; +} + +void Client::unsubscribe(const std::string &topic) +{ + LOG_DEBUG("Unsubscribing from topic '" + topic + "'"); +} diff --git a/src/Log.h b/src/Log.h new file mode 100644 index 0000000..1e875dc --- /dev/null +++ b/src/Log.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) TrueBrain + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +// Wrappers to make logging a tiny bit easier to read. +// It heavily depends on Client.cpp's structure, and assumes +// this->m_impl is reachable. + +#define LOGGER_LEVEL_ERROR 0 +#define LOGGER_LEVEL_WARN 1 +#define LOGGER_LEVEL_INFO 2 +#define LOGGER_LEVEL_DEBUG 3 +#define LOGGER_LEVEL_TRACE 4 + +// If no longer is defined, assume DEBUG level. +#ifndef MIN_LOGGER_LEVEL +#define MIN_LOGGER_LEVEL LOGGER_LEVEL_DEBUG +#endif + +#if MIN_LOGGER_LEVEL >= LOGGER_LEVEL_ERROR +#define LOG_ERROR(x) \ + if (this->m_impl->log_level >= Client::LogLevel::ERROR) \ + { \ + this->m_impl->logger(Client::LogLevel::ERROR, x); \ + } +#else +#define LOG_ERROR(x) +#endif + +#if MIN_LOGGER_LEVEL >= LOGGER_LEVEL_WARN +#define LOG_WARN(x) \ + if (this->m_impl->log_level >= Client::LogLevel::WARN) \ + { \ + this->m_impl->logger(Client::LogLevel::WARN, x); \ + } +#else +#define LOG_WARN(x) +#endif + +#if MIN_LOGGER_LEVEL >= LOGGER_LEVEL_INFO +#define LOG_INFO(x) \ + if (this->m_impl->log_level >= Client::LogLevel::INFO) \ + { \ + this->m_impl->logger(Client::LogLevel::INFO, x); \ + } +#else +#define LOG_INFO(x) +#endif + +#if MIN_LOGGER_LEVEL >= LOGGER_LEVEL_DEBUG +#define LOG_DEBUG(x) \ + if (this->m_impl->log_level >= Client::LogLevel::DEBUG) \ + { \ + this->m_impl->logger(Client::LogLevel::DEBUG, x); \ + } +#else +#define LOG_DEBUG(x) +#endif + +#if MIN_LOGGER_LEVEL >= LOGGER_LEVEL_TRACE +#define LOG_TRACE(x) \ + if (this->m_impl->log_level >= Client::LogLevel::TRACE) \ + { \ + this->m_impl->logger(Client::LogLevel::TRACE, x); \ + } +#else +#define LOG_TRACE(x) +#endif diff --git a/truemqtt.pc.in b/truemqtt.pc.in new file mode 100644 index 0000000..712df84 --- /dev/null +++ b/truemqtt.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=@CMAKE_INSTALL_PREFIX@ +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ + +Name: @PROJECT_NAME@ +Description: @PROJECT_DESCRIPTION@ +Version: @PROJECT_VERSION@ + +Requires: +Libs: -L${libdir} -ltruemqtt +Cflags: -I${includedir} -- libgit2 0.21.4