mirror of
https://github.com/espressif/esp-mqtt.git
synced 2026-08-03 20:14:15 +02:00
Merge branch 'fix-mqtt5-inflight-messages' into 'master'
MR: fix(mqtt5): Fix mqtt5 max inflight message counting Closes IDFGH-16316 See merge request espressif/esp-mqtt!308
This commit is contained in:
@@ -568,8 +568,12 @@ int esp_mqtt_client_unsubscribe(esp_mqtt_client_handle_t client,
|
||||
* (10s) or if publishing payloads longer than internal buffer (due to message
|
||||
* fragmentation)
|
||||
* - Client doesn't have to be connected for this API to work, enqueueing the
|
||||
* messages with qos>1 (returning -1 for all the qos=0 messages if
|
||||
* disconnected). If MQTT_SKIP_PUBLISH_IF_DISCONNECTED is enabled, this API will
|
||||
* messages with qos>0 (returning -1 for all the qos=0 messages if
|
||||
* disconnected).
|
||||
* - In case of MQTT v5, if the server quota for inflight messages is exceeded,
|
||||
* message will be enqueued and sent later when quota is available.
|
||||
* - QoS 0 messages are sent immediately in the calling task, not via the outbox.
|
||||
* - If MQTT_SKIP_PUBLISH_IF_DISCONNECTED is enabled, this API will
|
||||
* not attempt to publish when the client is not connected and will always
|
||||
* return -1.
|
||||
* - It is thread safe, please refer to `esp_mqtt_client_subscribe` for details
|
||||
@@ -597,6 +601,8 @@ int esp_mqtt_client_publish(esp_mqtt_client_handle_t client, const char *topic,
|
||||
* (in contrast to the esp_mqtt_client_publish() which sends the publish message
|
||||
* immediately in the user task's context). Thus, it could be used as a non
|
||||
* blocking version of esp_mqtt_client_publish().
|
||||
* - When MQTT v5 inflight quota is exceeded, queued QoS 1/2 messages are held
|
||||
* in the outbox. QoS 0 messages enqueued with store=true are not affected.
|
||||
*
|
||||
* @param client *MQTT* client handle
|
||||
* @param topic topic string
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#define _MQTT5_CLIENT_PRIV_H_
|
||||
|
||||
#include "mqtt5_client.h"
|
||||
#include "mqtt_client_priv.h"
|
||||
#include "mqtt5_msg.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -45,6 +44,7 @@ void esp_mqtt5_parse_suback(esp_mqtt5_client_handle_t client);
|
||||
void esp_mqtt5_parse_disconnect(esp_mqtt5_client_handle_t client, int *disconnect_rsp_code);
|
||||
esp_err_t esp_mqtt5_parse_connack(esp_mqtt5_client_handle_t client, int *connect_rsp_code);
|
||||
void esp_mqtt5_client_destory(esp_mqtt5_client_handle_t client);
|
||||
esp_err_t esp_mqtt5_client_check_inflight_maximum(esp_mqtt5_client_handle_t client);
|
||||
esp_err_t esp_mqtt5_client_publish_check(esp_mqtt5_client_handle_t client, int qos, int retain);
|
||||
esp_err_t esp_mqtt5_client_subscribe_check(esp_mqtt5_client_handle_t client, int qos);
|
||||
esp_err_t esp_mqtt5_create_default_config(esp_mqtt5_client_handle_t client);
|
||||
|
||||
+14
-10
@@ -10,6 +10,9 @@
|
||||
|
||||
static const char *TAG = "mqtt5_client";
|
||||
|
||||
// Receive Maximum is optional in CONNACK; when absent the limit is 65535
|
||||
#define MQTT5_DEFAULT_RECEIVE_MAXIMUM 65535
|
||||
|
||||
static void esp_mqtt5_print_error_code(esp_mqtt5_client_handle_t client, int code);
|
||||
static esp_err_t esp_mqtt5_client_update_topic_alias(mqtt5_topic_alias_handle_t topic_alias_handle,
|
||||
uint16_t topic_alias, char *topic, size_t topic_len);
|
||||
@@ -21,12 +24,8 @@ static esp_err_t esp_mqtt5_user_property_copy(mqtt5_user_property_handle_t user_
|
||||
|
||||
void esp_mqtt5_increment_packet_counter(esp_mqtt5_client_handle_t client)
|
||||
{
|
||||
bool msg_dup = mqtt5_get_dup(client->mqtt_state.connection.outbound_message.data);
|
||||
|
||||
if (msg_dup == false) {
|
||||
client->send_publish_packet_count ++;
|
||||
ESP_LOGD(TAG, "Sent (%d) qos > 0 publish packet without ack", client->send_publish_packet_count);
|
||||
}
|
||||
client->send_publish_packet_count ++;
|
||||
ESP_LOGD(TAG, "Sent (%d) qos > 0 publish packet without ack", client->send_publish_packet_count);
|
||||
}
|
||||
|
||||
void esp_mqtt5_decrement_packet_counter(esp_mqtt5_client_handle_t client)
|
||||
@@ -104,6 +103,7 @@ esp_err_t esp_mqtt5_parse_connack(esp_mqtt5_client_handle_t client, int *connect
|
||||
size_t len = client->mqtt_state.in_buffer_read_len;
|
||||
client->mqtt_state.in_buffer_read_len = 0;
|
||||
uint8_t ack_flag = 0;
|
||||
client->mqtt5_config->server_resp_property_info.receive_maximum = MQTT5_DEFAULT_RECEIVE_MAXIMUM;
|
||||
|
||||
if (mqtt5_msg_parse_connack_property(client->mqtt_state.in_buffer, len, &client->mqtt_state.
|
||||
connection.information, &client->mqtt5_config->connect_property_info, &client->mqtt5_config->server_resp_property_info,
|
||||
@@ -195,7 +195,7 @@ esp_err_t esp_mqtt5_create_default_config(esp_mqtt5_client_handle_t client)
|
||||
client->mqtt5_config->server_resp_property_info.wildcard_subscribe_available = true;
|
||||
client->mqtt5_config->server_resp_property_info.subscribe_identifiers_available = true;
|
||||
client->mqtt5_config->server_resp_property_info.shared_subscribe_available = true;
|
||||
client->mqtt5_config->server_resp_property_info.receive_maximum = 65535;
|
||||
client->mqtt5_config->server_resp_property_info.receive_maximum = MQTT5_DEFAULT_RECEIVE_MAXIMUM;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
@@ -375,9 +375,13 @@ esp_err_t esp_mqtt5_client_publish_check(esp_mqtt5_client_handle_t client, int q
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Flow control to check PUBLISH(No PUBACK or PUBCOMP received) packet sent count(Only record QoS1 and QoS2)*/
|
||||
if (client->send_publish_packet_count > client->mqtt5_config->server_resp_property_info.receive_maximum) {
|
||||
ESP_LOGE(TAG, "Client send more than %d QoS1 and QoS2 PUBLISH packet without no ack",
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_mqtt5_client_check_inflight_maximum(esp_mqtt5_client_handle_t client)
|
||||
{
|
||||
if (client->send_publish_packet_count >= client->mqtt5_config->server_resp_property_info.receive_maximum) {
|
||||
ESP_LOGD(TAG, "Broker quota for QoS > 0 exceeded. Quota is %d messages",
|
||||
client->mqtt5_config->server_resp_property_info.receive_maximum);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
+105
-42
@@ -843,6 +843,43 @@ static inline esp_err_t esp_mqtt_write(esp_mqtt_client_handle_t client)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
static void mqtt_requeue_transmitted_messages(esp_mqtt_client_handle_t client)
|
||||
{
|
||||
outbox_item_handle_t item;
|
||||
|
||||
// Receive Maximum is scoped to the network connection. Requeue previous
|
||||
// inflight packets so the new connection admits and counts them once.
|
||||
//
|
||||
// [MQTT-4.4.0-1] only sanctions resending unacknowledged QoS>0 PUBLISH and
|
||||
// PUBREL packets, so requeuing subscribe and unsubscribe is not correct.
|
||||
// It is kept because it is what the client has always done: the periodic
|
||||
// retransmit path resends any TRANSMITTED packet regardless of type, so
|
||||
// dropping them here would silently break subscriptions that work today.
|
||||
while ((item = outbox_dequeue(client->outbox, TRANSMITTED, NULL)) != NULL) {
|
||||
size_t len;
|
||||
uint16_t msg_id;
|
||||
int msg_type;
|
||||
int msg_qos;
|
||||
uint8_t *data = outbox_item_get_data(item, &len, &msg_id, &msg_type, &msg_qos);
|
||||
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to read transmitted outbox item");
|
||||
break;
|
||||
}
|
||||
|
||||
if (msg_type == MQTT_MSG_TYPE_PUBLISH && msg_qos > 0) {
|
||||
mqtt_set_dup(data);
|
||||
}
|
||||
|
||||
if (outbox_set_pending(client->outbox, msg_id, QUEUED) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to requeue transmitted message id=%d", msg_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static esp_err_t esp_mqtt_connect(esp_mqtt_client_handle_t client, int timeout_ms)
|
||||
{
|
||||
int read_len, connect_rsp_code = 0;
|
||||
@@ -909,6 +946,7 @@ static esp_err_t esp_mqtt_connect(esp_mqtt_client_handle_t client, int timeout_m
|
||||
|
||||
if (esp_mqtt5_parse_connack(client, &connect_rsp_code) == ESP_OK) {
|
||||
client->send_publish_packet_count = 0;
|
||||
mqtt_requeue_transmitted_messages(client);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -1690,14 +1728,14 @@ static esp_err_t mqtt_process_receive(esp_mqtt_client_handle_t client)
|
||||
break;
|
||||
|
||||
case MQTT_MSG_TYPE_PUBACK:
|
||||
if (remove_initiator_message(client, MQTT_MSG_TYPE_PUBLISH, msg_id)) {
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5) {
|
||||
esp_mqtt5_decrement_packet_counter(client);
|
||||
}
|
||||
|
||||
if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5) {
|
||||
esp_mqtt5_decrement_packet_counter(client);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
if (remove_initiator_message(client, MQTT_MSG_TYPE_PUBLISH, msg_id)) {
|
||||
ESP_LOGD(TAG, "received MQTT_MSG_TYPE_PUBACK, finish QoS1 publish");
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
esp_mqtt5_parse_puback(client);
|
||||
@@ -1753,15 +1791,15 @@ static esp_err_t mqtt_process_receive(esp_mqtt_client_handle_t client)
|
||||
|
||||
case MQTT_MSG_TYPE_PUBCOMP:
|
||||
ESP_LOGD(TAG, "received MQTT_MSG_TYPE_PUBCOMP");
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
|
||||
if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5) {
|
||||
esp_mqtt5_decrement_packet_counter(client);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
if (remove_initiator_message(client, MQTT_MSG_TYPE_PUBLISH, msg_id)) {
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
|
||||
if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5) {
|
||||
esp_mqtt5_decrement_packet_counter(client);
|
||||
}
|
||||
|
||||
#endif
|
||||
ESP_LOGD(TAG, "Receive MQTT_MSG_TYPE_PUBCOMP, finish QoS2 publish");
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
esp_mqtt5_parse_pubcomp(client);
|
||||
@@ -1832,6 +1870,25 @@ static esp_err_t mqtt_resend_queued(esp_mqtt_client_handle_t client, outbox_item
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
static outbox_item_handle_t mqtt_get_queued_qos0(outbox_handle_t outbox)
|
||||
{
|
||||
outbox_item_handle_t item = outbox_get(outbox, 0);
|
||||
size_t len;
|
||||
uint16_t msg_id;
|
||||
int msg_type;
|
||||
int msg_qos;
|
||||
|
||||
if (item && outbox_item_get_pending(item) == QUEUED &&
|
||||
outbox_item_get_data(item, &len, &msg_id, &msg_type, &msg_qos) != NULL &&
|
||||
msg_id == 0 && msg_type == MQTT_MSG_TYPE_PUBLISH && msg_qos == 0) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
static esp_err_t mqtt_resend_pubrel(esp_mqtt_client_handle_t client, outbox_item_handle_t item)
|
||||
{
|
||||
client->mqtt_state.connection.outbound_message.data = outbox_item_get_data(item,
|
||||
@@ -2021,6 +2078,23 @@ static void esp_mqtt_task(void *pv)
|
||||
|
||||
// resend all non-transmitted messages first
|
||||
outbox_item_handle_t item = outbox_dequeue(client->outbox, QUEUED, NULL);
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
|
||||
if (item && client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5 &&
|
||||
esp_mqtt5_client_check_inflight_maximum(client) != ESP_OK) {
|
||||
size_t len;
|
||||
uint16_t msg_id;
|
||||
int msg_type = 0;
|
||||
int msg_qos = 0;
|
||||
|
||||
if (outbox_item_get_data(item, &len, &msg_id, &msg_type, &msg_qos) != NULL &&
|
||||
msg_type == MQTT_MSG_TYPE_PUBLISH && msg_qos > 0) {
|
||||
// Receive Maximum applies only to QoS 1 and QoS 2.
|
||||
item = mqtt_get_queued_qos0(client->outbox);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
if (item) {
|
||||
if (mqtt_resend_queued(client, item) == ESP_OK) {
|
||||
@@ -2029,14 +2103,13 @@ static void esp_mqtt_task(void *pv)
|
||||
if (outbox_delete_item(client->outbox, item) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to remove queued qos0 message from the outbox");
|
||||
}
|
||||
}
|
||||
|
||||
if (client->mqtt_state.pending_publish_qos > 0 &&
|
||||
mqtt_get_type(client->mqtt_state.connection.outbound_message.data) == MQTT_MSG_TYPE_PUBLISH) {
|
||||
} else {
|
||||
outbox_set_tick(client->outbox, client->mqtt_state.pending_msg_id, platform_tick_get_ms());
|
||||
outbox_set_pending(client->outbox, client->mqtt_state.pending_msg_id, TRANSMITTED);
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
|
||||
if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5) {
|
||||
if (client->mqtt_state.pending_msg_type == MQTT_MSG_TYPE_PUBLISH &&
|
||||
client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5) {
|
||||
esp_mqtt5_increment_packet_counter(client);
|
||||
}
|
||||
|
||||
@@ -2050,36 +2123,13 @@ static void esp_mqtt_task(void *pv)
|
||||
item = outbox_dequeue(client->outbox, TRANSMITTED, &msg_tick);
|
||||
|
||||
if (item && (last_retransmit - msg_tick > client->config->message_retransmit_timeout)) {
|
||||
if (mqtt_resend_queued(client, item) == ESP_OK) {
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
|
||||
if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5 &&
|
||||
client->mqtt_state.pending_publish_qos > 0 &&
|
||||
mqtt_get_type(client->mqtt_state.connection.outbound_message.data) == MQTT_MSG_TYPE_PUBLISH) {
|
||||
esp_mqtt5_increment_packet_counter(client);
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
mqtt_resend_queued(client, item);
|
||||
}
|
||||
|
||||
item = outbox_dequeue(client->outbox, ACKNOWLEDGED, &msg_tick);
|
||||
|
||||
if (item && (last_retransmit - msg_tick > client->config->message_retransmit_timeout)) {
|
||||
if (mqtt_resend_pubrel(client, item) == ESP_OK) {
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
|
||||
// Do not count PUBREL as a new inflight PUBLISH
|
||||
// Only PUBLISH QoS>0 contributes to inflight limitation
|
||||
// (outbound_message here is PUBREL, so this condition will be false)
|
||||
if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5 &&
|
||||
client->mqtt_state.pending_publish_qos > 0 &&
|
||||
mqtt_get_type(client->mqtt_state.connection.outbound_message.data) == MQTT_MSG_TYPE_PUBLISH) {
|
||||
esp_mqtt5_increment_packet_counter(client);
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
mqtt_resend_pubrel(client, item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2311,6 +2361,8 @@ int esp_mqtt_client_subscribe_multiple(esp_mqtt_client_handle_t client,
|
||||
}
|
||||
|
||||
MQTT_API_LOCK(client);
|
||||
// Reset pending state to avoid inheriting previous PUBLISH QoS or type
|
||||
mqtt_reset_pending_message(client);
|
||||
|
||||
if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5) {
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
@@ -2572,6 +2624,17 @@ int esp_mqtt_client_publish(esp_mqtt_client_handle_t client, const char *topic,
|
||||
goto cannot_publish;
|
||||
}
|
||||
|
||||
#ifdef MQTT_PROTOCOL_5
|
||||
|
||||
if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5 && qos > 0) {
|
||||
if (esp_mqtt5_client_check_inflight_maximum(client) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Unable to publish now: maximum inflight messages reached");
|
||||
MQTT_API_UNLOCK(client);
|
||||
return pending_msg_id;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
/* Provide support for sending fragmented message if it doesn't fit buffer */
|
||||
int remaining_len = len;
|
||||
const char *current_data = data;
|
||||
|
||||
@@ -17,7 +17,7 @@ This app exposes a console API for pytest-embedded HIL tests that target MQTT co
|
||||
|
||||
## JSON config keys
|
||||
|
||||
All configuration is passed as a base64-encoded JSON object with exactly one top-level key, naming which config category the blob targets. The JSON shape mirrors the real esp_mqtt C struct layout, so field names/paths match `mqtt_client.h` / `mqtt5_client.h` directly.
|
||||
All configuration is passed as a base64-encoded JSON object with a recognized top-level key naming the config category the blob targets. The JSON shape mirrors the real esp_mqtt C struct layout, so field names/paths match `mqtt_client.h` / `mqtt5_client.h` directly.
|
||||
|
||||
### `mqtt_config` (used with `init`)
|
||||
|
||||
@@ -25,63 +25,67 @@ Mirrors `esp_mqtt_client_config_t`'s nesting:
|
||||
|
||||
```json
|
||||
{
|
||||
"mqtt_config": {
|
||||
"broker": { "address": { "uri": "mqtt://192.168.1.1:1883" } },
|
||||
"credentials": { "client_id": "my-client" },
|
||||
"session": { "keepalive": 30, "disable_clean_session": false, "protocol_ver": 3 },
|
||||
"network": { "disable_auto_reconnect": true }
|
||||
}
|
||||
"mqtt_config": {
|
||||
"broker": { "address": { "uri": "mqtt://192.168.1.1:1883" } },
|
||||
"credentials": { "client_id": "my-client" },
|
||||
"session": {
|
||||
"keepalive": 30,
|
||||
"disable_clean_session": false,
|
||||
"protocol_ver": 3
|
||||
},
|
||||
"network": { "disable_auto_reconnect": true }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Path | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `broker.address.uri` | string | Broker URI (e.g. `mqtt://192.168.1.1:1883`) |
|
||||
| `credentials.client_id` | string | Client identifier |
|
||||
| `session.keepalive` | int | Keepalive interval (seconds) |
|
||||
| `session.disable_clean_session` | bool | `true` = persistent session (clean start = false) |
|
||||
| `session.protocol_ver` | int | Raw `esp_mqtt_protocol_ver_t` ordinal: `0`=UNDEFINED, `1`=MQTT 3.1, `2`=MQTT 3.1.1, `3`=MQTT 5.0 |
|
||||
| `network.disable_auto_reconnect` | bool | Disable MQTT client automatic reconnect |
|
||||
| Path | Type | Description |
|
||||
| -------------------------------- | ------ | ------------------------------------------------------------------------------------------------ |
|
||||
| `broker.address.uri` | string | Broker URI (e.g. `mqtt://192.168.1.1:1883`) |
|
||||
| `credentials.client_id` | string | Client identifier |
|
||||
| `session.keepalive` | int | Keepalive interval (seconds) |
|
||||
| `session.disable_clean_session` | bool | `true` = persistent session (clean start = false) |
|
||||
| `session.protocol_ver` | int | Raw `esp_mqtt_protocol_ver_t` ordinal: `0`=UNDEFINED, `1`=MQTT 3.1, `2`=MQTT 3.1.1, `3`=MQTT 5.0 |
|
||||
| `network.disable_auto_reconnect` | bool | Disable MQTT client automatic reconnect |
|
||||
|
||||
### `connect_property` (MQTT5 connect properties)
|
||||
|
||||
Already flat in C, so the JSON object is flat too:
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `session_expiry_interval` | int | Session expiry (seconds) |
|
||||
| `receive_maximum` | int | Receive maximum |
|
||||
| `topic_alias_maximum` | int | Topic alias maximum |
|
||||
| `maximum_packet_size` | int | Maximum packet size |
|
||||
| `will_delay_interval` | int | Will delay interval (seconds) |
|
||||
| Key | Type | Description |
|
||||
| ------------------------- | ---- | ----------------------------- |
|
||||
| `session_expiry_interval` | int | Session expiry (seconds) |
|
||||
| `receive_maximum` | int | Receive maximum |
|
||||
| `topic_alias_maximum` | int | Topic alias maximum |
|
||||
| `maximum_packet_size` | int | Maximum packet size |
|
||||
| `will_delay_interval` | int | Will delay interval (seconds) |
|
||||
|
||||
### `publish_property` (MQTT5 publish properties)
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `message_expiry_interval` | int | Message expiry (seconds) |
|
||||
| `payload_format_indicator` | bool | `true` = UTF-8 encoded payload |
|
||||
| `topic_alias` | int | Topic alias |
|
||||
| `content_type` | string | Content type |
|
||||
| `response_topic` | string | Response topic |
|
||||
| Key | Type | Description |
|
||||
| -------------------------- | ------ | ------------------------------ |
|
||||
| `message_expiry_interval` | int | Message expiry (seconds) |
|
||||
| `payload_format_indicator` | bool | `true` = UTF-8 encoded payload |
|
||||
| `topic_alias` | int | Topic alias |
|
||||
| `content_type` | string | Content type |
|
||||
| `response_topic` | string | Response topic |
|
||||
|
||||
### `subscribe_property` (MQTT5 subscribe properties)
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `subscribe_id` | int | Subscription identifier |
|
||||
| `no_local_flag` | bool | No local flag |
|
||||
| `retain_as_published_flag` | bool | Retain as published flag |
|
||||
| `retain_handle` | int | Retain handling option (0/1/2) |
|
||||
| `is_share_subscribe` | bool | Shared subscription flag |
|
||||
| `share_name` | string | Shared subscription group name |
|
||||
| Key | Type | Description |
|
||||
| -------------------------- | ------ | ------------------------------ |
|
||||
| `subscribe_id` | int | Subscription identifier |
|
||||
| `no_local_flag` | bool | No local flag |
|
||||
| `retain_as_published_flag` | bool | Retain as published flag |
|
||||
| `retain_handle` | int | Retain handling option (0/1/2) |
|
||||
| `is_share_subscribe` | bool | Shared subscription flag |
|
||||
| `share_name` | string | Shared subscription group name |
|
||||
|
||||
### `disconnect_property` (MQTT5 disconnect properties)
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `session_expiry_interval` | int | Session expiry override on disconnect |
|
||||
| `disconnect_reason` | int | Disconnect reason code |
|
||||
| Key | Type | Description |
|
||||
| ------------------------- | ---- | ------------------------------------- |
|
||||
| `session_expiry_interval` | int | Session expiry override on disconnect |
|
||||
| `disconnect_reason` | int | Disconnect reason code |
|
||||
|
||||
## Conformance mapping
|
||||
|
||||
@@ -98,12 +102,37 @@ From the repository root (or the mqtt worktree root if using worktrees):
|
||||
1. Ensure the environment is active (e.g. `direnv allow` at repo root so IDF and pytest-embedded are available).
|
||||
|
||||
2. Initialize the paho.mqtt.testing submodule:
|
||||
```bash
|
||||
git submodule update --init --recursive test/tools/paho.mqtt.testing
|
||||
```
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive test/tools/paho.mqtt.testing
|
||||
```
|
||||
|
||||
3. Run the conformance tests (connect a board with Ethernet, or use the same target/port as in CI):
|
||||
```bash
|
||||
pytest test/apps/mqtt_conformance/ -v
|
||||
```
|
||||
To run a single test or filter by keyword, add e.g. `-k test_mqtt_v311` or the test path.
|
||||
|
||||
```bash
|
||||
pytest test/apps/mqtt_conformance/ -v
|
||||
```
|
||||
|
||||
To run a single test or filter by keyword, add e.g. `-k receive_maximum` or the test path.
|
||||
|
||||
## Optional environment variables
|
||||
|
||||
Each test starts its own fresh in-process paho broker on an OS-assigned ephemeral
|
||||
port and tears it down at the end of that test, so brokers never carry state
|
||||
between tests and there's no port to configure/coordinate.
|
||||
|
||||
- `MQTT_CONFORMANCE_PAHO_BROKER_LOG_LEVEL` — log level for the in-process paho broker's own
|
||||
logger (default: `WARNING`).
|
||||
- `MQTT_CONFORMANCE_HOST_IP` — host IPv4 address the DUT should use to reach the in-process
|
||||
broker (default: auto-detected via a UDP socket connect to `8.8.8.8`).
|
||||
- `MQTT_CONFORMANCE_CONNECT_RETRIES` — number of `start`/connect attempts before failing
|
||||
(default: 3).
|
||||
- `MQTT_CONFORMANCE_RETRY_BACKOFF_SEC` — backoff between connect retries, in seconds
|
||||
(default: 2).
|
||||
|
||||
### Timeouts
|
||||
|
||||
Tests use **operation-based timeouts** (not a flat 60 s wait): the budget is computed
|
||||
from the number of connect, subscribe, publish, and event-wait operations. Whole-test
|
||||
ceilings use `@pytest.mark.timeout(...)`. Inflight tests do not rely on timing windows:
|
||||
the broker explicitly holds and releases PUBACK or PUBCOMP packets around assertions.
|
||||
|
||||
@@ -410,7 +410,7 @@ void register_commands()
|
||||
|
||||
extern "C" void app_main(void)
|
||||
{
|
||||
constexpr size_t max_line = 512;
|
||||
constexpr size_t max_line = 2048;
|
||||
ESP_LOGI(TAG, "[APP] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size());
|
||||
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
|
||||
esp_log_level_set("*", ESP_LOG_INFO);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
idf_component_register(SRCS "test_mqtt_client.cpp" "test_log_intercept.cpp" "test_log_matchers.cpp" "test_log_parser.cpp"
|
||||
idf_component_register(SRCS "test_mqtt_client.cpp" "test_mqtt5_client.cpp" "mqtt5_client_test_adapter.c" "test_log_intercept.cpp" "test_log_matchers.cpp" "test_log_parser.cpp"
|
||||
REQUIRES cmock mqtt esp_timer esp_hw_support http_parser log
|
||||
WHOLE_ARCHIVE)
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <stdint.h>
|
||||
|
||||
#include "mqtt_client_priv.h"
|
||||
|
||||
esp_err_t test_mqtt5_check_inflight_maximum(uint16_t send_count, uint16_t receive_maximum)
|
||||
{
|
||||
struct esp_mqtt_client client = {0};
|
||||
mqtt5_config_storage_t mqtt5_config = {0};
|
||||
client.mqtt5_config = &mqtt5_config;
|
||||
client.mqtt5_config->server_resp_property_info.receive_maximum = receive_maximum;
|
||||
client.send_publish_packet_count = send_count;
|
||||
return esp_mqtt5_client_check_inflight_maximum(&client);
|
||||
}
|
||||
|
||||
int test_mqtt5_increment_packet_counter_with_dup(void)
|
||||
{
|
||||
struct esp_mqtt_client client = {0};
|
||||
uint8_t publish_header[] = {0x3a}; // PUBLISH, DUP=1, QoS=1
|
||||
client.mqtt_state.connection.outbound_message.data = publish_header;
|
||||
esp_mqtt5_increment_packet_counter(&client);
|
||||
return client.send_publish_packet_count;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
extern "C" {
|
||||
esp_err_t test_mqtt5_check_inflight_maximum(uint16_t send_count, uint16_t receive_maximum);
|
||||
int test_mqtt5_increment_packet_counter_with_dup(void);
|
||||
}
|
||||
|
||||
TEST_CASE("MQTT5 inflight quota uses an exact upper bound")
|
||||
{
|
||||
REQUIRE(test_mqtt5_check_inflight_maximum(1, 2) == ESP_OK);
|
||||
REQUIRE(test_mqtt5_check_inflight_maximum(2, 2) == ESP_FAIL);
|
||||
}
|
||||
|
||||
TEST_CASE("MQTT5 first send on a connection counts even when PUBLISH has DUP set")
|
||||
{
|
||||
REQUIRE(test_mqtt5_increment_packet_counter_with_dup() == 1);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
CONFIG_IDF_TARGET="linux"
|
||||
CONFIG_MQTT_PROTOCOL_5=y
|
||||
CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y
|
||||
CONFIG_COMPILER_CXX_EXCEPTIONS=y
|
||||
CONFIG_COMPILER_CXX_RTTI=y
|
||||
|
||||
@@ -204,6 +204,31 @@ TEST_CASE("Outbox lookup by msg_id")
|
||||
outbox_enqueue(outbox.handle, &message, 0);
|
||||
REQUIRE(outbox_get(outbox.handle, 999) == nullptr);
|
||||
}
|
||||
SECTION("msg_id zero finds queued QoS 0 behind a QoS 1 head") {
|
||||
auto qos1 = make_msg(1, 1, 3, "qos1", 4);
|
||||
auto qos0_first = make_msg(0, 0, 3, "first", 5);
|
||||
auto qos0_second = make_msg(0, 0, 3, "second", 6);
|
||||
outbox_enqueue(outbox.handle, &qos1, 0);
|
||||
outbox_enqueue(outbox.handle, &qos0_first, 0);
|
||||
outbox_enqueue(outbox.handle, &qos0_second, 0);
|
||||
REQUIRE(outbox_dequeue(outbox.handle, QUEUED, nullptr) == outbox_get(outbox.handle, 1));
|
||||
outbox_item_handle_t item = outbox_get(outbox.handle, 0);
|
||||
REQUIRE(item != nullptr);
|
||||
REQUIRE(outbox_item_get_pending(item) == QUEUED);
|
||||
uint16_t id;
|
||||
int type, qos;
|
||||
size_t len;
|
||||
auto *data = outbox_item_get_data(item, &len, &id, &type, &qos);
|
||||
REQUIRE(id == 0);
|
||||
REQUIRE(type == 3);
|
||||
REQUIRE(qos == 0);
|
||||
REQUIRE(std::string(reinterpret_cast<char *>(data), len) == "first");
|
||||
REQUIRE(outbox_delete_item(outbox.handle, item) == ESP_OK);
|
||||
item = outbox_get(outbox.handle, 0);
|
||||
REQUIRE(item != nullptr);
|
||||
data = outbox_item_get_data(item, &len, &id, &type, &qos);
|
||||
REQUIRE(std::string(reinterpret_cast<char *>(data), len) == "second");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Outbox delete by msg_id and type")
|
||||
|
||||
Reference in New Issue
Block a user