From d57b8c5b297c6223a285e390df281cf1eb20043f Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 11 Dec 2024 19:21:46 +0100 Subject: [PATCH 1/5] feat(mosq): Add example with two brokers synced on P2P Broker-less two chip example which virtual private IoT networks on MQTT protocol. --- .github/workflows/mosq__build.yml | 10 +- ci/check_copyright_ignore.txt | 1 + components/mosquitto/.build-test-rules.yml | 3 + .../examples/serverless_mqtt/CMakeLists.txt | 6 + .../examples/serverless_mqtt/README.md | 53 +++ .../components/libjuice/CMakeLists.txt | 44 +++ .../components/libjuice/include/ifaddrs.h | 13 + .../components/libjuice/port/juice_random.c | 40 ++ .../serverless_mqtt/main/CMakeLists.txt | 4 + .../serverless_mqtt/main/Kconfig.projbuild | 85 ++++ .../serverless_mqtt/main/idf_component.yml | 5 + .../serverless_mqtt/main/serverless_mqtt.c | 374 ++++++++++++++++++ .../serverless_mqtt/main/wifi_connect.c | 122 ++++++ .../serverless_mqtt/sdkconfig.defaults | 3 + .../examples/serverless_mqtt/serverless.png | Bin 0 -> 86433 bytes 15 files changed, 759 insertions(+), 4 deletions(-) create mode 100644 components/mosquitto/.build-test-rules.yml create mode 100644 components/mosquitto/examples/serverless_mqtt/CMakeLists.txt create mode 100644 components/mosquitto/examples/serverless_mqtt/README.md create mode 100644 components/mosquitto/examples/serverless_mqtt/components/libjuice/CMakeLists.txt create mode 100644 components/mosquitto/examples/serverless_mqtt/components/libjuice/include/ifaddrs.h create mode 100644 components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c create mode 100644 components/mosquitto/examples/serverless_mqtt/main/CMakeLists.txt create mode 100644 components/mosquitto/examples/serverless_mqtt/main/Kconfig.projbuild create mode 100644 components/mosquitto/examples/serverless_mqtt/main/idf_component.yml create mode 100644 components/mosquitto/examples/serverless_mqtt/main/serverless_mqtt.c create mode 100644 components/mosquitto/examples/serverless_mqtt/main/wifi_connect.c create mode 100644 components/mosquitto/examples/serverless_mqtt/sdkconfig.defaults create mode 100644 components/mosquitto/examples/serverless_mqtt/serverless.png diff --git a/.github/workflows/mosq__build.yml b/.github/workflows/mosq__build.yml index abf9dbb4bc..27ff2581c1 100644 --- a/.github/workflows/mosq__build.yml +++ b/.github/workflows/mosq__build.yml @@ -17,7 +17,8 @@ jobs: runs-on: ubuntu-22.04 container: espressif/idf:${{ matrix.idf_ver }} env: - TEST_DIR: components/mosquitto/examples/broker + TEST_DIR: components/mosquitto/examples + TARGET_TEST: broker TARGET_TEST_DIR: build_esp32_default steps: - name: Checkout esp-protocols @@ -29,14 +30,15 @@ jobs: run: | . ${IDF_PATH}/export.sh pip install idf-component-manager idf-build-apps --upgrade - python ci/build_apps.py ${TEST_DIR} - cd ${TEST_DIR} + python ci/build_apps.py -c ${TEST_DIR} -m components/mosquitto/.build-test-rules.yml + # upload only the target test artifacts + cd ${TEST_DIR}/${TARGET_TEST} ${GITHUB_WORKSPACE}/ci/clean_build_artifacts.sh `pwd`/${TARGET_TEST_DIR} zip -qur artifacts.zip ${TARGET_TEST_DIR} - uses: actions/upload-artifact@v4 with: name: mosq_target_esp32_${{ matrix.idf_ver }} - path: ${{ env.TEST_DIR }}/artifacts.zip + path: ${{ env.TEST_DIR }}/${{ env.TARGET_TEST }}/artifacts.zip if-no-files-found: error test_mosq: diff --git a/ci/check_copyright_ignore.txt b/ci/check_copyright_ignore.txt index e69de29bb2..1cd8798f5b 100644 --- a/ci/check_copyright_ignore.txt +++ b/ci/check_copyright_ignore.txt @@ -0,0 +1 @@ +components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c diff --git a/components/mosquitto/.build-test-rules.yml b/components/mosquitto/.build-test-rules.yml new file mode 100644 index 0000000000..e1f5846485 --- /dev/null +++ b/components/mosquitto/.build-test-rules.yml @@ -0,0 +1,3 @@ +components/mosquitto/examples/serverless_mqtt: + disable: + - if: IDF_TARGET not in ["esp32", "esp32s3", "esp32c3"] diff --git a/components/mosquitto/examples/serverless_mqtt/CMakeLists.txt b/components/mosquitto/examples/serverless_mqtt/CMakeLists.txt new file mode 100644 index 0000000000..c9935c9567 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(serverless_mqtt) diff --git a/components/mosquitto/examples/serverless_mqtt/README.md b/components/mosquitto/examples/serverless_mqtt/README.md new file mode 100644 index 0000000000..5ee8369e67 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/README.md @@ -0,0 +1,53 @@ +# Brokerless MQTT Example + +MQTT served by (two) mosquitto's running on two ESP chips. + +* Leverages MQTT connectivity between two private networks without cloud premisses. +* Creates two local MQTT servers (on ESP32x's) which are being synchronized over peer to peer connection (established via ICE protocol, by [libjuice](https://github.com/paullouisageneau/libjuice)). + +## How it works + +This example needs two ESP32 chipsets, that will create two separate Wi-Fi networks (IoT networks) used for IoT devices. +Each IoT network is served by an MQTT server (using mosquitto component). +This example will also synchronize these two MQTT brokers, as if there was only one IoT network with one broker. +This example creates a peer to peer connection between two chipsets to keep them synchronize. This connection utilizes libjuice (which implements a simplified ICE-UDP) to traverse NATs, which enabling direct connection between two private networks behind NATs. + +* Diagram + +![demo](serverless.png) + +Here's a step-by-step procedure of establishing this remote connection: +1) Initialize and start Wi-Fi AP (for IoT networks) and Wi-Fi station (for internet connection) +2) Start mosquitto broker on IoT network +3) Start libjuice to gather connection candidates +4) Synchronize using a public MQTT broker and exchange ICE descriptors +5) Establish ICE UDP connection between the two ESP32 chipsets +6) Start forwarding mqtt messages + - Each remote datagram (received from ICE-UDP channel) is re-published to the local MQTT server + - Each local MQTT message (received from mosquitto on_message callback) is sent in ICE-UDP datagram + +## How to use this example + +You need two ESP32 devices that support Wi-Fi station and Wi-Fi software access point. + +* Configure Wi-Fi credentials for both devices on both interfaces + * These devices would be deployed in distinct Wi-Fi environments, so the Wi-Fi station credentials would likely be different. + * They also create their own IoT network (on the soft-AP interface) Wi-Fi, so the AP credentials would likely be the same, suggesting the IoT networks will be keep synchronized (even though these are two distict Wi-Fi networks). +* Choose `CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER1` for one device and `CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER2` for another. It's not important which device is PEER1, since the code is symmetric, but these two devices need to have different role. +* Optionally: You can use `idf.py` `-D` and `-B` flag to keep separate build directories and sdkconfigs for these two roles +``` +idf.py -B build1 -DSDKCONFIG=build1/sdkconfig menuconfig build flash monitor +``` +* Flash and run the two devices and wait for them to connect and synchronize. +* Now you can test MQTT connectivity, for example: + * Join PEER1 device's AP and connect to the MQTT broker with one or more clients, subscribing to one or more topics. + * Join PEER2 device's AP and connect to the MQTT broker with one or more clients, subscribing to one or more topics. + * Whenever you publish to a topic, all subscribed clients should receive the message, no matter which Wi-Fi network they're connected to. + +## Warning + +This example uses libjuice as a dependency: + +* libjuice (UDP Interactive Connectivity Establishment): https://github.com/paullouisageneau/libjuice + +which is distributed under Mozilla Public License v2.0. diff --git a/components/mosquitto/examples/serverless_mqtt/components/libjuice/CMakeLists.txt b/components/mosquitto/examples/serverless_mqtt/components/libjuice/CMakeLists.txt new file mode 100644 index 0000000000..f7cf012d1b --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/components/libjuice/CMakeLists.txt @@ -0,0 +1,44 @@ +set(LIBJUICE_VERSION "73785387eafe15c02b6a210edb10f722474e8e14") +set(LIBJUICE_URL "https://github.com/paullouisageneau/libjuice/archive/${LIBJUICE_VERSION}.zip") + +set(libjuice_dir ${CMAKE_BINARY_DIR}/libjuice/libjuice-${LIBJUICE_VERSION}) + +# Fetch the library +if(NOT EXISTS ${libjuice_dir}) + message(STATUS "Downloading libjuice ${LIBJUICE_VERSION}...") + file(DOWNLOAD ${LIBJUICE_URL} ${CMAKE_BINARY_DIR}/libjuice.zip SHOW_PROGRESS) + execute_process(COMMAND unzip -o ${CMAKE_BINARY_DIR}/libjuice.zip -d ${CMAKE_BINARY_DIR}/libjuice + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +endif() + +set(JUICE_SOURCES ${libjuice_dir}/src/addr.c + ${libjuice_dir}/src/agent.c + ${libjuice_dir}/src/base64.c + ${libjuice_dir}/src/conn.c + ${libjuice_dir}/src/conn_mux.c + ${libjuice_dir}/src/conn_poll.c + ${libjuice_dir}/src/conn_thread.c + ${libjuice_dir}/src/const_time.c + ${libjuice_dir}/src/crc32.c + ${libjuice_dir}/src/hash.c + ${libjuice_dir}/src/ice.c + ${libjuice_dir}/src/juice.c + ${libjuice_dir}/src/log.c + ${libjuice_dir}/src/server.c + ${libjuice_dir}/src/stun.c + ${libjuice_dir}/src/timestamp.c + ${libjuice_dir}/src/turn.c + ${libjuice_dir}/src/udp.c +# Use hmac from mbedtls and random numbers from esp_random: +# ${libjuice_dir}/src/hmac.c +# ${libjuice_dir}/src/random.c + ) + +idf_component_register(SRCS port/juice_random.c + ${JUICE_SOURCES} + INCLUDE_DIRS "include" "${libjuice_dir}/include" "${libjuice_dir}/include/juice" + REQUIRES esp_netif + PRIV_REQUIRES sock_utils) + +target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") +set_source_files_properties(${libjuice_dir}/src/udp.c PROPERTIES COMPILE_FLAGS -Wno-unused-variable) diff --git a/components/mosquitto/examples/serverless_mqtt/components/libjuice/include/ifaddrs.h b/components/mosquitto/examples/serverless_mqtt/components/libjuice/include/ifaddrs.h new file mode 100644 index 0000000000..ba92bc72b8 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/components/libjuice/include/ifaddrs.h @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once + +// Purpose of this header is to replace udp_sendto() to avoid name conflict with lwip +// added here since ifaddrs.h is included from juice_udp sources +#define udp_sendto juice_udp_sendto + +// other than that, let's just include the ifaddrs (from sock_utils) +#include_next "ifaddrs.h" diff --git a/components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c b/components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c new file mode 100644 index 0000000000..89c1c6bdaa --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +#include "esp_random.h" + +void juice_random(void *buf, size_t size) +{ + esp_fill_random(buf, size); +} + +void juice_random_str64(char *buf, size_t size) +{ + static const char chars64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t i = 0; + for (i = 0; i + 1 < size; ++i) { + uint8_t byte = 0; + juice_random(&byte, 1); + buf[i] = chars64[byte & 0x3F]; + } + buf[i] = '\0'; +} + +uint32_t juice_rand32(void) +{ + uint32_t r = 0; + juice_random(&r, sizeof(r)); + return r; +} + +uint64_t juice_rand64(void) +{ + uint64_t r = 0; + juice_random(&r, sizeof(r)); + return r; +} diff --git a/components/mosquitto/examples/serverless_mqtt/main/CMakeLists.txt b/components/mosquitto/examples/serverless_mqtt/main/CMakeLists.txt new file mode 100644 index 0000000000..b757b72863 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "serverless_mqtt.c" + "wifi_connect.c" + INCLUDE_DIRS "." + REQUIRES libjuice nvs_flash mqtt json esp_wifi) diff --git a/components/mosquitto/examples/serverless_mqtt/main/Kconfig.projbuild b/components/mosquitto/examples/serverless_mqtt/main/Kconfig.projbuild new file mode 100644 index 0000000000..7e0e97de03 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/Kconfig.projbuild @@ -0,0 +1,85 @@ +menu "Example Configuration" + + menu "AP Configuration" + comment "AP Configuration" + + config EXAMPLE_AP_SSID + string "Wi-Fi SSID" + default "myssid" + help + Set the SSID of Wi-Fi ap interface. + + config EXAMPLE_AP_PASSWORD + string "Wi-Fi Password" + default "12345678" + help + Set the password of Wi-Fi ap interface. + + endmenu + + menu "STA Configuration" + comment "STA Configuration" + + config EXAMPLE_STA_SSID + string "WiFi Station SSID" + default "mystationssid" + help + SSID for the example's sta to connect to. + + config EXAMPLE_STA_PASSWORD + string "WiFi Station Password" + default "mystationpassword" + help + WiFi station password for the example to use. + endmenu + + config EXAMPLE_MQTT_BROKER_URI + string "MQTT Broker URL" + default "mqtt://mqtt.eclipseprojects.io" + help + URL of the mqtt broker use for synchronisation and exchanging + ICE connect info (description and candidates). + + config EXAMPLE_MQTT_SYNC_TOPIC + string "MQTT topic for synchronisation" + default "/topic/serverless_mqtt" + help + MQTT topic used fo synchronisation. + + config EXAMPLE_STUN_SERVER + string "Hostname of STUN server" + default "stun.l.google.com" + help + STUN server hostname. + + config EXAMPLE_MQTT_CLIENT_STACK_SIZE + int "Stack size for mqtt client" + default 16384 + help + Set stack size for the mqtt client. + Need more stack, since calling juice API from the handler. + + config EXAMPLE_MQTT_BROKER_PORT + int "port for the mosquitto to listen to" + default 1883 + help + This is a port which the local mosquitto uses. + + choice EXAMPLE_SERVERLESS_ROLE + prompt "Choose your role" + default EXAMPLE_SERVERLESS_ROLE_PEER1 + help + Choose either peer1 or peer2. + It's not very important which device is peer1 + (peer-1 sends sync messages, peer2 listens for them) + It is important that we have two peers, + one with peer1 config, another one with peer2 config + + config EXAMPLE_SERVERLESS_ROLE_PEER1 + bool "peer1" + + config EXAMPLE_SERVERLESS_ROLE_PEER2 + bool "peer2" + endchoice + +endmenu diff --git a/components/mosquitto/examples/serverless_mqtt/main/idf_component.yml b/components/mosquitto/examples/serverless_mqtt/main/idf_component.yml new file mode 100644 index 0000000000..e3297d5351 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/idf_component.yml @@ -0,0 +1,5 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mosquitto: + override_path: ../../.. + espressif/sock_utils: "*" diff --git a/components/mosquitto/examples/serverless_mqtt/main/serverless_mqtt.c b/components/mosquitto/examples/serverless_mqtt/main/serverless_mqtt.c new file mode 100644 index 0000000000..8dd0bee7c7 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/serverless_mqtt.c @@ -0,0 +1,374 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "mqtt_client.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_random.h" +#include "esp_check.h" +#include "esp_sleep.h" +#include "mosq_broker.h" +#include "juice/juice.h" +#include "cJSON.h" + +#if defined(CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER1) +#define OUR_PEER "1" +#define THEIR_PEER "2" +#elif defined(CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER2) +#define OUR_PEER "2" +#define THEIR_PEER "1" +#endif + +#define PEER_SYNC0 BIT(0) +#define PEER_SYNC1 BIT(1) +#define PEER_SYNC2 BIT(2) +#define PEER_FAIL BIT(3) +#define PEER_GATHER_DONE BIT(4) +#define PEER_DESC_PUBLISHED BIT(5) +#define PEER_CONNECTED BIT(6) + +#define SYNC_BITS (PEER_SYNC1 | PEER_SYNC2 | PEER_FAIL) + +#define PUBLISH_SYNC_TOPIC CONFIG_EXAMPLE_MQTT_SYNC_TOPIC OUR_PEER +#define SUBSCRIBE_SYNC_TOPIC CONFIG_EXAMPLE_MQTT_SYNC_TOPIC THEIR_PEER +#define MAX_BUFFER_SIZE JUICE_MAX_SDP_STRING_LEN + +typedef struct message_wrap { + uint16_t topic_len; + uint16_t data_len; + char data[]; +} __attribute__((packed)) message_wrap_t; + +static const char *TAG = "serverless_mqtt" OUR_PEER; +static char s_buffer[MAX_BUFFER_SIZE]; +static EventGroupHandle_t s_state = NULL; +static juice_agent_t *s_agent = NULL; +static cJSON *s_peer_desc_json = NULL; +static char *s_peer_desc = NULL; +static esp_mqtt_client_handle_t s_local_mqtt = NULL; + +char *wifi_get_ipv4(wifi_interface_t interface); +esp_err_t wifi_connect(void); +static esp_err_t sync_peers(void); +static esp_err_t create_candidates(void); +static esp_err_t create_local_client(void); +static esp_err_t create_local_broker(void); + +void app_main(void) +{ + __attribute__((__unused__)) esp_err_t ret; + ESP_GOTO_ON_ERROR(wifi_connect(), err, TAG, "Failed to initialize WiFi"); + ESP_GOTO_ON_ERROR(create_local_broker(), err, TAG, "Failed to create local broker"); + ESP_GOTO_ON_ERROR(create_candidates(), err, TAG, "Failed to create juice candidates"); + ESP_GOTO_ON_ERROR(sync_peers(), err, TAG, "Failed to sync with the other peer"); + EventBits_t bits = xEventGroupWaitBits(s_state, PEER_FAIL | PEER_CONNECTED, pdFALSE, pdFALSE, pdMS_TO_TICKS(90000)); + if (bits & PEER_CONNECTED) { + ESP_LOGI(TAG, "Peer is connected!"); + ESP_GOTO_ON_ERROR(create_local_client(), err, TAG, "Failed to create forwarding mqtt client"); + ESP_LOGI(TAG, "Everything is ready, exiting main task"); + return; + } +err: + ESP_LOGE(TAG, "Non recoverable error, going to sleep for some time (random, max 20s)"); + esp_deep_sleep(1000000LL * (esp_random() % 20)); +} + +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_mqtt_event_handle_t event = event_data; + esp_mqtt_client_handle_t client = event->client; + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); + if (esp_mqtt_client_subscribe(client, SUBSCRIBE_SYNC_TOPIC, 1) < 0) { + ESP_LOGE(TAG, "Failed to subscribe to the sync topic"); + } + xEventGroupSetBits(s_state, PEER_SYNC0); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); + xEventGroupSetBits(s_state, PEER_FAIL); + break; + + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT_EVENT_DATA"); + printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); + printf("DATA=%.*s\r\n", event->data_len, event->data); + if (s_state == NULL || memcmp(event->topic, SUBSCRIBE_SYNC_TOPIC, event->topic_len) != 0) { + break; + } + EventBits_t bits = xEventGroupGetBits(s_state); + if (event->data_len > 1 && s_agent) { + cJSON *root = cJSON_Parse(event->data); + if (root == NULL) { + break; + } + cJSON *desc = cJSON_GetObjectItem(root, "desc"); + if (desc == NULL) { + cJSON_Delete(root); + break; + } + printf("desc->valuestring:%s\n", desc->valuestring); + juice_set_remote_description(s_agent, desc->valuestring); + char cand_name[] = "cand0"; + while (true) { + cJSON *cand = cJSON_GetObjectItem(root, cand_name); + if (cand == NULL) { + break; + } + printf("%s: cand->valuestring:%s\n", cand_name, cand->valuestring); + juice_add_remote_candidate(s_agent, cand->valuestring); + cand_name[4]++; + } + cJSON_Delete(root); + xEventGroupSetBits(s_state, PEER_DESC_PUBLISHED); // this will complete the sync process + // and destroy the mqtt client + } +#ifdef CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER1 + if (event->data_len == 1 && event->data[0] == '1' && (bits & PEER_SYNC2) == 0) { + if (esp_mqtt_client_publish(client, PUBLISH_SYNC_TOPIC, "2", 1, 1, 0) >= 0) { + xEventGroupSetBits(s_state, PEER_SYNC2); + } else { + xEventGroupSetBits(s_state, PEER_FAIL); + } + } +#else + if (event->data_len == 1 && event->data[0] == '0' && (bits & PEER_SYNC1) == 0) { + if (esp_mqtt_client_publish(client, PUBLISH_SYNC_TOPIC, "1", 1, 1, 0) >= 0) { + xEventGroupSetBits(s_state, PEER_SYNC1); + } else { + xEventGroupSetBits(s_state, PEER_FAIL); + } + } else if (event->data_len == 1 && event->data[0] == '2' && (bits & PEER_SYNC2) == 0) { + xEventGroupSetBits(s_state, PEER_SYNC2); + } +#endif + break; + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); + xEventGroupSetBits(s_state, PEER_FAIL); + break; + default: + ESP_LOGI(TAG, "Other event id:%d", event->event_id); + break; + } +} + +static esp_err_t sync_peers(void) +{ + esp_err_t ret = ESP_OK; + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.uri = CONFIG_EXAMPLE_MQTT_BROKER_URI, + .task.stack_size = CONFIG_EXAMPLE_MQTT_CLIENT_STACK_SIZE, + }; + esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); + ESP_GOTO_ON_FALSE(client, ESP_ERR_NO_MEM, err, TAG, "Failed to create mqtt client"); + ESP_GOTO_ON_ERROR(esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL), + err, TAG, "Failed to register mqtt event handler"); + ESP_GOTO_ON_ERROR(esp_mqtt_client_start(client), err, TAG, "Failed to start mqtt client"); + ESP_GOTO_ON_FALSE(xEventGroupWaitBits(s_state, PEER_SYNC0, pdTRUE, pdTRUE, pdMS_TO_TICKS(10000)), + ESP_FAIL, err, TAG, "Failed to connect to the sync broker"); + ESP_LOGI(TAG, "Waiting for the other peer..."); + const int max_sync_retry = 60; + int retry = 0; + while (true) { + EventBits_t bits = xEventGroupWaitBits(s_state, SYNC_BITS, pdTRUE, pdFALSE, pdMS_TO_TICKS(1000)); + if (bits & PEER_SYNC2) { + break; + } + if (bits & PEER_SYNC1) { + continue; + } + ESP_GOTO_ON_FALSE((bits & PEER_FAIL) == 0, ESP_FAIL, err, TAG, "Failed to sync with the other peer"); + ESP_GOTO_ON_FALSE(retry++ < max_sync_retry, ESP_FAIL, err, TAG, "Failed to sync after %d seconds", retry); +#ifdef CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER1 + ESP_RETURN_ON_FALSE(esp_mqtt_client_publish(client, PUBLISH_SYNC_TOPIC, "0", 1, 1, 0) >= 0, + ESP_FAIL, TAG, "Failed to publish mqtt message"); +#endif + } + ESP_LOGI(TAG, "Sync done"); + ESP_RETURN_ON_FALSE(esp_mqtt_client_publish(client, PUBLISH_SYNC_TOPIC, s_peer_desc, 0, 1, 0) >= 0, + ESP_FAIL, TAG, "Failed to publish peer's description"); + ESP_LOGI(TAG, "Waiting for the other peer description and candidates..."); + ESP_GOTO_ON_FALSE(xEventGroupWaitBits(s_state, PEER_DESC_PUBLISHED, pdTRUE, pdTRUE, pdMS_TO_TICKS(10000)), + ESP_FAIL, err, TAG, "Timeout in waiting for the other peer candidates"); +err: + free(s_peer_desc); + esp_mqtt_client_destroy(client); + return ret; +} + +static void juice_state(juice_agent_t *agent, juice_state_t state, void *user_ptr) +{ + ESP_LOGI(TAG, "JUICE state change: %s", juice_state_to_string(state)); + if (state == JUICE_STATE_CONNECTED) { + xEventGroupSetBits(s_state, PEER_CONNECTED); + } else if (state == JUICE_STATE_FAILED || state == JUICE_STATE_DISCONNECTED) { + esp_restart(); + } +} + +static void juice_candidate(juice_agent_t *agent, const char *sdp, void *user_ptr) +{ + static uint8_t cand_nr = 0; + if (s_peer_desc_json && cand_nr < 10) { // supporting only 10 candidates + char cand_name[] = "cand0"; + cand_name[4] += cand_nr++; + cJSON_AddStringToObject(s_peer_desc_json, cand_name, sdp); + } +} + +static void juice_gathering_done(juice_agent_t *agent, void *user_ptr) +{ + ESP_LOGI(TAG, "Gathering done"); + if (s_state) { + xEventGroupSetBits(s_state, PEER_GATHER_DONE); + } +} + +#define ALIGN(size) (((size) + 3U) & ~(3U)) + +static void juice_recv(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) +{ + if (s_local_mqtt) { + message_wrap_t *message = (message_wrap_t *)data; + int topic_len = message->topic_len; + int payload_len = message->data_len; + int topic_len_aligned = ALIGN(topic_len); + char *topic = message->data; + char *payload = message->data + topic_len_aligned; + if (topic_len + topic_len_aligned + 4 > size) { + ESP_LOGE(TAG, "Received invalid message"); + return; + } + ESP_LOGI(TAG, "forwarding remote message: topic:%s", topic); + ESP_LOGI(TAG, "forwarding remote message: payload:%.*s", payload_len, payload); + esp_mqtt_client_publish(s_local_mqtt, topic, payload, payload_len, 0, 0); + } +} + +static esp_err_t create_candidates(void) +{ + ESP_RETURN_ON_FALSE(s_state = xEventGroupCreate(), ESP_ERR_NO_MEM, TAG, "Failed to create state event group"); + s_peer_desc_json = cJSON_CreateObject(); + esp_err_t ret = ESP_OK; + juice_set_log_level(JUICE_LOG_LEVEL_INFO); + juice_config_t config = { .stun_server_host = CONFIG_EXAMPLE_STUN_SERVER, + .bind_address = wifi_get_ipv4(WIFI_IF_STA), + .stun_server_port = 19302, + .cb_state_changed = juice_state, + .cb_candidate = juice_candidate, + .cb_gathering_done = juice_gathering_done, + .cb_recv = juice_recv, + }; + + s_agent = juice_create(&config); + ESP_RETURN_ON_FALSE(s_agent, ESP_FAIL, TAG, "Failed to create juice agent"); + ESP_GOTO_ON_FALSE(juice_get_local_description(s_agent, s_buffer, MAX_BUFFER_SIZE) == JUICE_ERR_SUCCESS, + ESP_FAIL, err, TAG, "Failed to get local description"); + ESP_LOGI(TAG, "desc: %s", s_buffer); + cJSON_AddStringToObject(s_peer_desc_json, "desc", s_buffer); + + ESP_GOTO_ON_FALSE(juice_gather_candidates(s_agent) == JUICE_ERR_SUCCESS, + ESP_FAIL, err, TAG, "Failed to start gathering candidates"); + ESP_GOTO_ON_FALSE(xEventGroupWaitBits(s_state, PEER_GATHER_DONE, pdTRUE, pdTRUE, pdMS_TO_TICKS(30000)), + ESP_FAIL, err, TAG, "Failed to connect to the sync broker"); + s_peer_desc = cJSON_Print(s_peer_desc_json); + ESP_LOGI(TAG, "desc: %s", s_peer_desc); + cJSON_Delete(s_peer_desc_json); + return ESP_OK; + +err: + juice_destroy(s_agent); + s_agent = NULL; + cJSON_Delete(s_peer_desc_json); + s_peer_desc_json = NULL; + return ret; +} + +static void local_handler(void *args, esp_event_base_t base, int32_t id, void *data) +{ + switch (id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "local client connected"); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "local client disconnected"); + break; + + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "local client error"); + break; + default: + ESP_LOGI(TAG, "local client event id:%d", (int)id); + break; + } +} + +static esp_err_t create_local_client(void) +{ + esp_err_t ret = ESP_OK; + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.transport = MQTT_TRANSPORT_OVER_TCP, + .broker.address.hostname = wifi_get_ipv4(WIFI_IF_AP), + .broker.address.port = CONFIG_EXAMPLE_MQTT_BROKER_PORT, + .task.stack_size = CONFIG_EXAMPLE_MQTT_CLIENT_STACK_SIZE, + .credentials.client_id = "local_mqtt" + }; + s_local_mqtt = esp_mqtt_client_init(&mqtt_cfg); + ESP_GOTO_ON_FALSE(s_local_mqtt, ESP_ERR_NO_MEM, err, TAG, "Failed to create mqtt client"); + ESP_GOTO_ON_ERROR(esp_mqtt_client_register_event(s_local_mqtt, ESP_EVENT_ANY_ID, local_handler, NULL), + err, TAG, "Failed to register mqtt event handler"); + ESP_GOTO_ON_ERROR(esp_mqtt_client_start(s_local_mqtt), err, TAG, "Failed to start mqtt client"); + + return ESP_OK; +err: + esp_mqtt_client_destroy(s_local_mqtt); + s_local_mqtt = NULL; + return ret; +} + +static void handle_message(char *client, char *topic, char *payload, int len, int qos, int retain) +{ + if (client && strcmp(client, "local_mqtt") == 0 ) { + // This is our little local client -- do not forward + return; + } + ESP_LOGI(TAG, "handle_message topic:%s", topic); + ESP_LOGI(TAG, "handle_message data:%.*s", len, payload); + ESP_LOGI(TAG, "handle_message qos=%d, retain=%d", qos, retain); + if (s_local_mqtt && s_agent) { + int topic_len = strlen(topic) + 1; // null term + int topic_len_aligned = ALIGN(topic_len); + int total_msg_len = 2 + 2 /* msg_wrap header */ + topic_len_aligned + len; + if (total_msg_len > MAX_BUFFER_SIZE) { + ESP_LOGE(TAG, "Fail to forward, message too long"); + return; + } + message_wrap_t *message = (message_wrap_t *)s_buffer; + message->topic_len = topic_len; + message->data_len = len; + + memcpy(s_buffer + 4, topic, topic_len); + memcpy(s_buffer + 4 + topic_len_aligned, payload, len); + juice_send(s_agent, s_buffer, total_msg_len); + } +} + +static void broker_task(void *ctx) +{ + struct mosq_broker_config config = { .host = wifi_get_ipv4(WIFI_IF_AP), .port = CONFIG_EXAMPLE_MQTT_BROKER_PORT, .handle_message_cb = handle_message }; + mosq_broker_run(&config); + vTaskDelete(NULL); +} + +static esp_err_t create_local_broker(void) +{ + return xTaskCreate(broker_task, "mqtt_broker_task", 1024 * 32, NULL, 5, NULL) == pdTRUE ? + ESP_OK : ESP_FAIL; +} diff --git a/components/mosquitto/examples/serverless_mqtt/main/wifi_connect.c b/components/mosquitto/examples/serverless_mqtt/main/wifi_connect.c new file mode 100644 index 0000000000..aeb3af7599 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/wifi_connect.c @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "nvs_flash.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_check.h" +#include "esp_wifi.h" +#include "esp_mac.h" + +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static const char *TAG = "serverless_wifi"; +static EventGroupHandle_t s_wifi_events; +static int s_retry_num = 0; +static const int s_max_retry = 30; + +static void wifi_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + if (event_id == WIFI_EVENT_AP_STACONNECTED) { + wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *) event_data; + ESP_LOGI(TAG, "station "MACSTR" join, AID=%d", + MAC2STR(event->mac), event->aid); + } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) { + wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *) event_data; + ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d", + MAC2STR(event->mac), event->aid); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + if (s_retry_num < s_max_retry) { + esp_wifi_connect(); + s_retry_num++; + ESP_LOGI(TAG, "retry to connect to the AP"); + } else { + xEventGroupSetBits(s_wifi_events, WIFI_FAIL_BIT); + } + ESP_LOGI(TAG, "Connect to the AP fail"); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; + ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + s_retry_num = 0; + xEventGroupSetBits(s_wifi_events, WIFI_CONNECTED_BIT); + } +} + +esp_err_t wifi_connect(void) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(s_wifi_events = xEventGroupCreate(), ESP_ERR_NO_MEM, err, TAG, "Failed to create wifi_events"); + ESP_GOTO_ON_ERROR(nvs_flash_init(), err, TAG, "Failed to init nvs flash"); + ESP_GOTO_ON_ERROR(esp_netif_init(), err, TAG, "Failed to init esp_netif"); + ESP_GOTO_ON_ERROR(esp_event_loop_create_default(), err, TAG, "Failed to create default event loop"); + ESP_GOTO_ON_ERROR(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL), + err, TAG, "Failed to register WiFi event handler"); + ESP_GOTO_ON_ERROR(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL), + err, TAG, "Failed to register IP event handler"); + + // Initialize WiFi + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_GOTO_ON_ERROR(esp_wifi_init(&cfg), err, TAG, "Failed to initialize WiFi"); + ESP_GOTO_ON_ERROR(esp_wifi_set_mode(WIFI_MODE_APSTA), err, TAG, "Failed to set STA+AP mode"); + + // Initialize AP + esp_netif_t *ap = esp_netif_create_default_wifi_ap(); + ESP_GOTO_ON_FALSE(ap, ESP_FAIL, err, TAG, "Failed to create AP network interface"); + wifi_config_t wifi_ap_config = { + .ap = { + .ssid = CONFIG_EXAMPLE_AP_SSID, + .password = CONFIG_EXAMPLE_AP_PASSWORD, + .authmode = WIFI_AUTH_WPA2_PSK, + .max_connection = 4, + }, + }; + ESP_GOTO_ON_ERROR(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config), err, TAG, "Failed to set AP config"); + + + // Initialize STA + esp_netif_t *sta = esp_netif_create_default_wifi_sta(); + ESP_GOTO_ON_FALSE(sta, ESP_FAIL, err, TAG, "Failed to create WiFi station network interface"); + wifi_config_t wifi_sta_config = { + .sta = { + .ssid = CONFIG_EXAMPLE_STA_SSID, + .password = CONFIG_EXAMPLE_STA_PASSWORD, + }, + }; + ESP_GOTO_ON_ERROR(esp_wifi_set_config(WIFI_IF_STA, &wifi_sta_config), err, TAG, "Failed to set STA config"); + + // Start WiFi + ESP_GOTO_ON_ERROR(esp_wifi_start(), err, TAG, "Failed to start WiFi"); + + // Wait for connection + EventBits_t bits = xEventGroupWaitBits(s_wifi_events, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, pdFALSE, pdMS_TO_TICKS(30000)); + ESP_GOTO_ON_FALSE((bits & WIFI_CONNECTED_BIT) == WIFI_CONNECTED_BIT, ESP_FAIL, err, + TAG, "Failed to obtain IP address from WiFi station"); + return ESP_OK; +err: + esp_wifi_stop(); + esp_wifi_deinit(); + nvs_flash_deinit(); + esp_netif_deinit(); + esp_event_loop_delete_default(); + return ret; + +} + +_Thread_local char s_ipv4_addr[4 * 4]; // 4 octets + '.'/term + +char *wifi_get_ipv4(wifi_interface_t interface) +{ + esp_netif_t *netif = esp_netif_get_handle_from_ifkey(interface == WIFI_IF_AP ? "WIFI_AP_DEF" : "WIFI_STA_DEF"); + ESP_RETURN_ON_FALSE(netif, NULL, TAG, "Failed to find default Wi-Fi netif"); + esp_netif_ip_info_t ip_info; + ESP_RETURN_ON_FALSE(esp_netif_get_ip_info(netif, &ip_info) == ESP_OK, NULL, TAG, "Failed to get IP from netif"); + ESP_RETURN_ON_FALSE(esp_ip4addr_ntoa(&ip_info.ip, s_ipv4_addr, sizeof(s_ipv4_addr)) != NULL, NULL, TAG, "Failed to convert IP"); + return s_ipv4_addr; +} diff --git a/components/mosquitto/examples/serverless_mqtt/sdkconfig.defaults b/components/mosquitto/examples/serverless_mqtt/sdkconfig.defaults new file mode 100644 index 0000000000..037b680153 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=32768 diff --git a/components/mosquitto/examples/serverless_mqtt/serverless.png b/components/mosquitto/examples/serverless_mqtt/serverless.png new file mode 100644 index 0000000000000000000000000000000000000000..6822526427e764d6b0d8377665240eae41e77cf8 GIT binary patch literal 86433 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 TDnScRfRB!*!L{P6R>A)QP(mnb literal 0 HcmV?d00001 From 6cce87e4652bb690ac230dfacd8111e6d4dd0029 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 19 Dec 2024 16:33:48 +0100 Subject: [PATCH 2/5] fix(mosq): Fix dependency issues moving esp-tls to public deps Since esp-tls structs are using in public header files --- components/mosquitto/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/mosquitto/CMakeLists.txt b/components/mosquitto/CMakeLists.txt index 1db843ec19..47e5d551d9 100644 --- a/components/mosquitto/CMakeLists.txt +++ b/components/mosquitto/CMakeLists.txt @@ -81,7 +81,8 @@ idf_component_register(SRCS ${m_srcs} PRIV_INCLUDE_DIRS port/priv_include port/priv_include/sys ${m_dir} ${m_src_dir} ${m_incl_dir} ${m_lib_dir} ${m_deps_dir} INCLUDE_DIRS ${m_incl_dir} port/include - PRIV_REQUIRES newlib esp-tls + REQUIRES esp-tls + PRIV_REQUIRES newlib ) target_compile_definitions(${COMPONENT_LIB} PRIVATE "WITH_BROKER") From cdeab8f517923e2f6a2a9352d7fecbcb6d4af88f Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 19 Dec 2024 16:36:51 +0100 Subject: [PATCH 3/5] feat(mosq): Add support for on-message callback --- components/mosquitto/port/broker.c | 5 +++++ components/mosquitto/port/callbacks.c | 5 +++++ components/mosquitto/port/include/mosq_broker.h | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/components/mosquitto/port/broker.c b/components/mosquitto/port/broker.c index 62cfb29bce..f78f651790 100644 --- a/components/mosquitto/port/broker.c +++ b/components/mosquitto/port/broker.c @@ -101,6 +101,8 @@ void mosq_broker_stop(void) run = 0; } +extern mosq_message_cb_t g_mosq_message_callback; + int mosq_broker_run(struct mosq_broker_config *broker_config) { @@ -125,6 +127,9 @@ int mosq_broker_run(struct mosq_broker_config *broker_config) if (broker_config->tls_cfg) { net__set_tls_config(broker_config->tls_cfg); } + if (broker_config->handle_message_cb) { + g_mosq_message_callback = broker_config->handle_message_cb; + } db.config = &config; diff --git a/components/mosquitto/port/callbacks.c b/components/mosquitto/port/callbacks.c index cf6e5f9a29..dc0f2ad1b5 100644 --- a/components/mosquitto/port/callbacks.c +++ b/components/mosquitto/port/callbacks.c @@ -13,7 +13,9 @@ #include "util_mosq.h" #include "utlist.h" #include "lib_load.h" +#include "mosq_broker.h" +mosq_message_cb_t g_mosq_message_callback = NULL; int mosquitto_callback_register( mosquitto_plugin_id_t *identifier, @@ -44,5 +46,8 @@ void plugin__handle_disconnect(struct mosquitto *context, int reason) int plugin__handle_message(struct mosquitto *context, struct mosquitto_msg_store *stored) { + if (g_mosq_message_callback) { + g_mosq_message_callback(context->id, stored->topic, stored->payload, stored->payloadlen, stored->qos, stored->retain); + } return MOSQ_ERR_SUCCESS; } diff --git a/components/mosquitto/port/include/mosq_broker.h b/components/mosquitto/port/include/mosq_broker.h index 362943557f..d9b858b8c4 100644 --- a/components/mosquitto/port/include/mosq_broker.h +++ b/components/mosquitto/port/include/mosq_broker.h @@ -9,6 +9,7 @@ struct mosquitto__config; +typedef void (*mosq_message_cb_t)(char *client, char *topic, char *data, int len, int qos, int retain); /** * @brief Mosquitto configuration structure * @@ -24,6 +25,10 @@ struct mosq_broker_config { * You can open the respective docs with this idf.py command: * `idf.py docs -sp api-reference/protocols/esp_tls.html` */ + void (*handle_message_cb)(char *client, char *topic, char *data, int len, int qos, int retain); /*!< + * On message callback. If configured, user function is called + * whenever mosquitto processes a message. + */ }; /** From 3b2c614d8669acace15ac67452589b0adba9c7c5 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 19 Dec 2024 16:38:41 +0100 Subject: [PATCH 4/5] feat(mosq): Upgrade to mosquitto v2.0.20 Used tagged version v2.0.20 --- components/mosquitto/mosquitto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/mosquitto/mosquitto b/components/mosquitto/mosquitto index 3923526c6b..a196c2b244 160000 --- a/components/mosquitto/mosquitto +++ b/components/mosquitto/mosquitto @@ -1 +1 @@ -Subproject commit 3923526c6b4c048bbecad2506c4c9963bc46cd36 +Subproject commit a196c2b244f248072a6b3ac8fb3f00ce0ff63dea From e6fb8aa078464b59c9fd38fec246df3911611630 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 19 Dec 2024 16:45:04 +0100 Subject: [PATCH 5/5] bump(mosq): 2.0.18~0 -> 2.0.20~0 2.0.20~0 Features - Upgrade to mosquitto v2.0.20 (3b2c614d) - Add support for on-message callback (cdeab8f5) - Add example with two brokers synced on P2P (d57b8c5b) Bug Fixes - Fix dependency issues moving esp-tls to public deps (6cce87e4) --- components/mosquitto/.cz.yaml | 2 +- components/mosquitto/CHANGELOG.md | 18 ++++++++++++++++++ components/mosquitto/idf_component.yml | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/components/mosquitto/.cz.yaml b/components/mosquitto/.cz.yaml index f9b741cd0b..0b6507a505 100644 --- a/components/mosquitto/.cz.yaml +++ b/components/mosquitto/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(mosq): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py mosquitto tag_format: mosq-v$version - version: 2.0.28~0 + version: 2.0.20 version_files: - idf_component.yml diff --git a/components/mosquitto/CHANGELOG.md b/components/mosquitto/CHANGELOG.md index bcc1c9a553..5c24047c45 100644 --- a/components/mosquitto/CHANGELOG.md +++ b/components/mosquitto/CHANGELOG.md @@ -1,5 +1,23 @@ +# Changelog + +## [2.0.20](https://github.com/espressif/esp-protocols/commits/mosq-v2.0.20) + +### Features + +- Upgrade to mosquitto v2.0.20 ([3b2c614d](https://github.com/espressif/esp-protocols/commit/3b2c614d)) +- Add support for on-message callback ([cdeab8f5](https://github.com/espressif/esp-protocols/commit/cdeab8f5)) +- Add example with two brokers synced on P2P ([d57b8c5b](https://github.com/espressif/esp-protocols/commit/d57b8c5b)) + +### Bug Fixes + +- Fix dependency issues moving esp-tls to public deps ([6cce87e4](https://github.com/espressif/esp-protocols/commit/6cce87e4)) + ## [2.0.28~0](https://github.com/espressif/esp-protocols/commits/mosq-v2.0.28_0) +### Warning + +Incorrect version number! This version published under `2.0.28~0` is based on upstream v2.0.18 + ### Features - Added support for TLS transport using ESP-TLS ([1af4bbe1](https://github.com/espressif/esp-protocols/commit/1af4bbe1)) diff --git a/components/mosquitto/idf_component.yml b/components/mosquitto/idf_component.yml index 5ef670047f..d8aee0e68a 100644 --- a/components/mosquitto/idf_component.yml +++ b/components/mosquitto/idf_component.yml @@ -1,4 +1,4 @@ -version: "2.0.28~0" +version: "2.0.20~0" url: https://github.com/espressif/esp-protocols/tree/master/components/mosquitto description: The component provides a simple ESP32 port of mosquitto broker dependencies: