diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1fce7fb --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,18 @@ +set(srcs mqtt_client.c lib/mqtt_msg.c lib/mqtt_outbox.c lib/platform_esp32_idf.c) + +if(CONFIG_MQTT_PROTOCOL_5) + list(APPEND srcs lib/mqtt5_msg.c mqtt5_client.c) +endif() + +list(TRANSFORM srcs PREPEND ${CMAKE_CURRENT_LIST_DIR}/) +idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/include + PRIV_INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/lib/include + REQUIRES esp_event tcp_transport + PRIV_REQUIRES esp_timer http_parser esp_hw_support heap + KCONFIG ${CMAKE_CURRENT_LIST_DIR}/Kconfig + ) +target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") + + + diff --git a/Kconfig b/Kconfig new file mode 100644 index 0000000..113bac7 --- /dev/null +++ b/Kconfig @@ -0,0 +1,166 @@ +menu "ESP-MQTT Configurations" + + config MQTT_PROTOCOL_311 + bool "Enable MQTT protocol 3.1.1" + default y + help + If not, this library will use MQTT protocol 3.1 + + config MQTT_PROTOCOL_5 + bool "Enable MQTT protocol 5.0" + default n + help + If not, this library will not support MQTT 5.0 + + config MQTT_TRANSPORT_SSL + bool "Enable MQTT over SSL" + default y + help + Enable MQTT transport over SSL with mbedtls + + config MQTT_TRANSPORT_WEBSOCKET + bool "Enable MQTT over Websocket" + default y + depends on WS_TRANSPORT + help + Enable MQTT transport over Websocket. + + config MQTT_TRANSPORT_WEBSOCKET_SECURE + bool "Enable MQTT over Websocket Secure" + default y + depends on MQTT_TRANSPORT_WEBSOCKET + depends on MQTT_TRANSPORT_SSL + help + Enable MQTT transport over Websocket Secure. + + config MQTT_MSG_ID_INCREMENTAL + bool "Use Incremental Message Id" + default n + help + Set this to true for the message id (2.3.1 Packet Identifier) to be generated + as an incremental number rather then a random value (used by default) + + config MQTT_SKIP_PUBLISH_IF_DISCONNECTED + bool "Skip publish if disconnected" + default n + help + Set this to true to avoid publishing (enqueueing messages) if the client is disconnected. + The MQTT client tries to publish all messages by default, even in the disconnected state + (where the qos1 and qos2 packets are stored in the internal outbox to be published later) + The MQTT_SKIP_PUBLISH_IF_DISCONNECTED option allows applications to override this behaviour + and not enqueue publish packets in the disconnected state. + + config MQTT_REPORT_DELETED_MESSAGES + bool "Report deleted messages" + default n + help + Set this to true to post events for all messages which were deleted from the outbox + before being correctly sent and confirmed. + + config MQTT_USE_CUSTOM_CONFIG + bool "MQTT Using custom configurations" + default n + help + Custom MQTT configurations. + + config MQTT_TCP_DEFAULT_PORT + int "Default MQTT over TCP port" + default 1883 + depends on MQTT_USE_CUSTOM_CONFIG + help + Default MQTT over TCP port + + config MQTT_SSL_DEFAULT_PORT + int "Default MQTT over SSL port" + default 8883 + depends on MQTT_USE_CUSTOM_CONFIG + depends on MQTT_TRANSPORT_SSL + help + Default MQTT over SSL port + + config MQTT_WS_DEFAULT_PORT + int "Default MQTT over Websocket port" + default 80 + depends on MQTT_USE_CUSTOM_CONFIG + depends on MQTT_TRANSPORT_WEBSOCKET + help + Default MQTT over Websocket port + + config MQTT_WSS_DEFAULT_PORT + int "Default MQTT over Websocket Secure port" + default 443 + depends on MQTT_USE_CUSTOM_CONFIG + depends on MQTT_TRANSPORT_WEBSOCKET + depends on MQTT_TRANSPORT_WEBSOCKET_SECURE + help + Default MQTT over Websocket Secure port + + config MQTT_BUFFER_SIZE + int "Default MQTT Buffer Size" + default 1024 + depends on MQTT_USE_CUSTOM_CONFIG + help + This buffer size using for both transmit and receive + + config MQTT_TASK_STACK_SIZE + int "MQTT task stack size" + default 6144 + depends on MQTT_USE_CUSTOM_CONFIG + help + MQTT task stack size + + config MQTT_DISABLE_API_LOCKS + bool "Disable API locks" + default n + depends on MQTT_USE_CUSTOM_CONFIG + help + Default config employs API locks to protect internal structures. It is possible to disable + these locks if the user code doesn't access MQTT API from multiple concurrent tasks + + config MQTT_TASK_PRIORITY + int "MQTT task priority" + default 5 + depends on MQTT_USE_CUSTOM_CONFIG + help + MQTT task priority. Higher number denotes higher priority. + + config MQTT_EVENT_QUEUE_SIZE + int "Number of queued events." + default 1 + depends on MQTT_USE_CUSTOM_CONFIG + help + A value higher than 1 enables multiple queued events. + + config MQTT_TASK_CORE_SELECTION_ENABLED + bool "Enable MQTT task core selection" + help + This will enable core selection + + choice MQTT_TASK_CORE_SELECTION + depends on MQTT_TASK_CORE_SELECTION_ENABLED + prompt "Core to use ?" + config MQTT_USE_CORE_0 + bool "Core 0" + config MQTT_USE_CORE_1 + bool "Core 1" + endchoice + + config MQTT_CUSTOM_OUTBOX + bool "Enable custom outbox implementation" + default n + help + Set to true if a specific implementation of message outbox is needed (e.g. persistent outbox in NVM or + similar). + Note: Implementation of the custom outbox must be added to the mqtt component. These CMake commands + could be used to append the custom implementation to lib-mqtt sources: + idf_component_get_property(mqtt mqtt COMPONENT_LIB) + set_property(TARGET ${mqtt} PROPERTY SOURCES ${PROJECT_DIR}/custom_outbox.c APPEND) + + config MQTT_OUTBOX_EXPIRED_TIMEOUT_MS + int "Outbox message expired timeout[ms]" + default 30000 + depends on MQTT_USE_CUSTOM_CONFIG + help + Messages which stays in the outbox longer than this value before being published will be discarded. + +endmenu diff --git a/host_test/CMakeLists.txt b/host_test/CMakeLists.txt new file mode 100644 index 0000000..db98733 --- /dev/null +++ b/host_test/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) +list(APPEND EXTRA_COMPONENT_DIRS + "mocks/heap/" + "$ENV{IDF_PATH}/tools/mocks/esp_hw_support/" + "$ENV{IDF_PATH}/tools/mocks/freertos/" + "$ENV{IDF_PATH}/tools/mocks/esp_timer/" + "$ENV{IDF_PATH}/tools/mocks/esp_event/" + "$ENV{IDF_PATH}/tools/mocks/lwip/" + "$ENV{IDF_PATH}/tools/mocks/esp-tls/" + "$ENV{IDF_PATH}/tools/mocks/http_parser/" + "$ENV{IDF_PATH}/tools/mocks/tcp_transport/" + ) + +project(host_mqtt_client_test) diff --git a/host_test/README.md b/host_test/README.md new file mode 100644 index 0000000..a62087a --- /dev/null +++ b/host_test/README.md @@ -0,0 +1,30 @@ +| Supported Targets | Linux | +| ----------------- | ----- | + +# Description + +This directory contains test code for the mqtt client that runs on host. + +Tests are written using [Catch2](https://github.com/catchorg/Catch2) test framework + +# Build + +Tests build regularly like an idf project. + +``` +idf.py build +``` + +# Run + +The build produces an executable in the build folder. + +Just run: + +``` +./build/host_mqtt_client_test.elf +``` + +The test executable have some options provided by the test framework. + + diff --git a/host_test/main/CMakeLists.txt b/host_test/main/CMakeLists.txt new file mode 100644 index 0000000..6c4a9c7 --- /dev/null +++ b/host_test/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "test_mqtt_client.cpp" + INCLUDE_DIRS "$ENV{IDF_PATH}/tools/catch" + REQUIRES cmock mqtt esp_timer esp_hw_support http_parser log) diff --git a/host_test/main/test_mqtt_client.cpp b/host_test/main/test_mqtt_client.cpp new file mode 100644 index 0000000..37dd34e --- /dev/null +++ b/host_test/main/test_mqtt_client.cpp @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#define CATCH_CONFIG_MAIN // This tells the catch header to generate a main +#include "catch.hpp" + +extern "C" { +#include "Mockesp_event.h" +#include "Mockesp_mac.h" +#include "Mockesp_transport.h" +#include "Mockesp_transport_ssl.h" +#include "Mockesp_transport_tcp.h" +#include "Mockesp_transport_ws.h" +#include "Mockevent_groups.h" +#include "Mockhttp_parser.h" +#include "Mockqueue.h" +#include "Mocktask.h" +#include "Mockesp_timer.h" + + /* + * The following functions are not directly called but the generation of them + * from cmock is broken, so we need to define them here. + */ + esp_err_t esp_tls_get_and_clear_last_error(esp_tls_error_handle_t h, int *esp_tls_code, int *esp_tls_flags) + { + return ESP_OK; + } +} + +#include "mqtt_client.h" + +struct ClientInitializedFixture { + esp_mqtt_client_handle_t client; + ClientInitializedFixture() + { + [[maybe_unused]] auto protect = TEST_PROTECT(); + int mtx; + int transport_list; + int transport; + int event_group; + uint8_t mac[] = {0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55}; + esp_timer_get_time_IgnoreAndReturn(0); + xQueueTakeMutexRecursive_IgnoreAndReturn(true); + xQueueGiveMutexRecursive_IgnoreAndReturn(true); + xQueueCreateMutex_ExpectAnyArgsAndReturn( + reinterpret_cast(&mtx)); + xEventGroupCreate_IgnoreAndReturn(reinterpret_cast(&event_group)); + esp_transport_list_init_IgnoreAndReturn(reinterpret_cast(&transport_list)); + esp_transport_tcp_init_IgnoreAndReturn(reinterpret_cast(&transport)); + esp_transport_ssl_init_IgnoreAndReturn(reinterpret_cast(&transport)); + esp_transport_ws_init_IgnoreAndReturn(reinterpret_cast(&transport)); + esp_transport_ws_set_subprotocol_IgnoreAndReturn(ESP_OK); + esp_transport_list_add_IgnoreAndReturn(ESP_OK); + esp_transport_set_default_port_IgnoreAndReturn(ESP_OK); + http_parser_parse_url_IgnoreAndReturn(0); + http_parser_url_init_ExpectAnyArgs(); + esp_event_loop_create_IgnoreAndReturn(ESP_OK); + esp_read_mac_IgnoreAndReturn(ESP_OK); + esp_read_mac_ReturnThruPtr_mac(mac); + esp_transport_list_destroy_IgnoreAndReturn(ESP_OK); + vEventGroupDelete_Ignore(); + vQueueDelete_Ignore(); + + esp_mqtt_client_config_t config{}; + client = esp_mqtt_client_init(&config); + } + ~ClientInitializedFixture() + { + esp_mqtt_client_destroy(client); + } +}; +TEST_CASE_METHOD(ClientInitializedFixture, "Client set uri") +{ + struct http_parser_url ret_uri = { + .field_set = 1, + .port = 0, + .field_data = { { 0, 1} } + }; + SECTION("User set a correct URI") { + http_parser_parse_url_StopIgnore(); + http_parser_parse_url_ExpectAnyArgsAndReturn(0); + http_parser_parse_url_ReturnThruPtr_u(&ret_uri); + auto res = esp_mqtt_client_set_uri(client, " "); + REQUIRE(res == ESP_OK); + } + SECTION("Incorrect URI from user") { + http_parser_parse_url_StopIgnore(); + http_parser_parse_url_ExpectAnyArgsAndReturn(1); + http_parser_parse_url_ReturnThruPtr_u(&ret_uri); + auto res = esp_mqtt_client_set_uri(client, " "); + REQUIRE(res == ESP_FAIL); + } +} +TEST_CASE_METHOD(ClientInitializedFixture, "Client Start") +{ + SECTION("Successful start") { + esp_mqtt_client_config_t config{}; + config.broker.address.uri = "mqtt://1.1.1.1"; + struct http_parser_url ret_uri = { + .field_set = 1 | (1<<1), + .port = 0, + .field_data = { { 0, 4 } /*mqtt*/, { 7, 1 } } // at least *scheme* and *host* + }; + http_parser_parse_url_StopIgnore(); + http_parser_parse_url_ExpectAnyArgsAndReturn(0); + http_parser_parse_url_ReturnThruPtr_u(&ret_uri); + xTaskCreatePinnedToCore_ExpectAnyArgsAndReturn(pdTRUE); + auto res = esp_mqtt_set_config(client, &config); + REQUIRE(res == ESP_OK); + res = esp_mqtt_client_start(client); + REQUIRE(res == ESP_OK); + } + SECTION("Failed on initialization") { + xTaskCreatePinnedToCore_ExpectAnyArgsAndReturn(pdFALSE); + auto res = esp_mqtt_client_start(nullptr); + REQUIRE(res == ESP_ERR_INVALID_ARG); + } + SECTION("Client already started") {} + SECTION("Failed to start task") { + xTaskCreatePinnedToCore_ExpectAnyArgsAndReturn(pdFALSE); + auto res = esp_mqtt_client_start(client); + REQUIRE(res == ESP_FAIL); + } +} diff --git a/host_test/mocks/heap/CMakeLists.txt b/host_test/mocks/heap/CMakeLists.txt new file mode 100644 index 0000000..0a0c8a6 --- /dev/null +++ b/host_test/mocks/heap/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_get_property(original_heap_dir heap COMPONENT_OVERRIDEN_DIR) + +idf_component_register(SRCS heap_mock.c + INCLUDE_DIRS "${original_heap_dir}/include") diff --git a/host_test/mocks/heap/heap_mock.c b/host_test/mocks/heap/heap_mock.c new file mode 100644 index 0000000..6bc3b35 --- /dev/null +++ b/host_test/mocks/heap/heap_mock.c @@ -0,0 +1,11 @@ +#include "esp_heap_caps.h" +#include +#include + + + +void *heap_caps_calloc(size_t n, size_t size, uint32_t caps) { + (void)caps; + return calloc(n, size); + +} diff --git a/host_test/mocks/include/sys/queue.h b/host_test/mocks/include/sys/queue.h new file mode 100644 index 0000000..5ec7fec --- /dev/null +++ b/host_test/mocks/include/sys/queue.h @@ -0,0 +1,66 @@ +#pragma once + +/* Implementation from BSD headers*/ +#define QMD_SAVELINK(name, link) void **name = (void *)&(link) +#define TRASHIT(x) do {(x) = (void *)-1;} while (0) +#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next) + +#define STAILQ_FIRST(head) ((head)->stqh_first) + +#define STAILQ_HEAD(name, type) \ +struct name { \ + struct type *stqh_first;/* first element */ \ + struct type **stqh_last;/* addr of last next element */ \ +} + +#define STAILQ_ENTRY(type) \ +struct { \ + struct type *stqe_next; /* next element */ \ +} + +#define STAILQ_INSERT_TAIL(head, elm, field) do { \ + STAILQ_NEXT((elm), field) = NULL; \ + *(head)->stqh_last = (elm); \ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ +} while (0) + +#define STAILQ_INIT(head) do { \ + STAILQ_FIRST((head)) = NULL; \ + (head)->stqh_last = &STAILQ_FIRST((head)); \ +} while (0) + +#define STAILQ_FOREACH(var, head, field) \ + for((var) = STAILQ_FIRST((head)); \ + (var); \ + (var) = STAILQ_NEXT((var), field)) + +#define STAILQ_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = STAILQ_FIRST((head)); \ + (var) && ((tvar) = STAILQ_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define STAILQ_REMOVE_AFTER(head, elm, field) do { \ + if ((STAILQ_NEXT(elm, field) = \ + STAILQ_NEXT(STAILQ_NEXT(elm, field), field)) == NULL) \ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ +} while (0) + +#define STAILQ_REMOVE_HEAD(head, field) do { \ + if ((STAILQ_FIRST((head)) = \ + STAILQ_NEXT(STAILQ_FIRST((head)), field)) == NULL) \ + (head)->stqh_last = &STAILQ_FIRST((head)); \ +} while (0) + +#define STAILQ_REMOVE(head, elm, type, field) do { \ + QMD_SAVELINK(oldnext, (elm)->field.stqe_next); \ + if (STAILQ_FIRST((head)) == (elm)) { \ + STAILQ_REMOVE_HEAD((head), field); \ + } \ + else { \ + struct type *curelm = STAILQ_FIRST((head)); \ + while (STAILQ_NEXT(curelm, field) != (elm)) \ + curelm = STAILQ_NEXT(curelm, field); \ + STAILQ_REMOVE_AFTER(head, curelm, field); \ + } \ + TRASHIT(*oldnext); \ +} while (0) diff --git a/host_test/sdkconfig.defaults b/host_test/sdkconfig.defaults new file mode 100644 index 0000000..c126429 --- /dev/null +++ b/host_test/sdkconfig.defaults @@ -0,0 +1,6 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_COMPILER_CXX_RTTI=y +CONFIG_COMPILER_CXX_EXCEPTIONS_EMG_POOL_SIZE=0 +CONFIG_COMPILER_STACK_CHECK_NONE=y +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=n diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..23cec31 --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,5 @@ +version: "1.0.0" +description: esp-mqtt +dependencies: + idf: + version: ">=5.0" diff --git a/mqtt_client.c b/mqtt_client.c index d732d95..3a2563c 100644 --- a/mqtt_client.c +++ b/mqtt_client.c @@ -235,6 +235,10 @@ esp_mqtt_set_transport_failed: /* Checks if the user supplied config values are internally consistent */ static esp_err_t esp_mqtt_check_cfg_conflict(const mqtt_config_storage_t *cfg, const esp_mqtt_client_config_t *user_cfg) { + if(cfg == NULL || user_cfg == NULL) { + ESP_LOGE(TAG, "Invalid configuration"); + return ESP_ERR_INVALID_ARG; + } esp_err_t ret = ESP_OK; bool ssl_cfg_enabled = cfg->use_global_ca_store || cfg->cacert_buf || cfg->clientcert_buf || cfg->psk_hint_key || cfg->alpn_protos; @@ -517,6 +521,7 @@ esp_err_t esp_mqtt_set_config(esp_mqtt_client_handle_t client, const esp_mqtt_cl if (config->broker.address.transport) { free(client->config->scheme); + client->config->scheme = NULL; if (config->broker.address.transport == MQTT_TRANSPORT_OVER_TCP) { client->config->scheme = create_string(MQTT_OVER_TCP_SCHEME, strlen(MQTT_OVER_TCP_SCHEME)); ESP_MEM_CHECK(TAG, client->config->scheme, goto _mqtt_set_config_failed);