fix(mqtt5): Fix mqtt5 max inflight message counting

Split message validation path for mqtt5
Introduces tests on the conformance test suite

Fixes #312
This commit is contained in:
Euripedes Rocha Filho
2026-05-06 10:47:59 +02:00
parent 827875a087
commit 20b6eb0e30
12 changed files with 1198 additions and 155 deletions
+8 -2
View File
@@ -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 * (10s) or if publishing payloads longer than internal buffer (due to message
* fragmentation) * fragmentation)
* - Client doesn't have to be connected for this API to work, enqueueing the * - 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 * messages with qos>0 (returning -1 for all the qos=0 messages if
* disconnected). If MQTT_SKIP_PUBLISH_IF_DISCONNECTED is enabled, this API will * 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 * not attempt to publish when the client is not connected and will always
* return -1. * return -1.
* - It is thread safe, please refer to `esp_mqtt_client_subscribe` for details * - 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 * (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 * immediately in the user task's context). Thus, it could be used as a non
* blocking version of esp_mqtt_client_publish(). * 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 client *MQTT* client handle
* @param topic topic string * @param topic topic string
+1 -1
View File
@@ -8,7 +8,6 @@
#define _MQTT5_CLIENT_PRIV_H_ #define _MQTT5_CLIENT_PRIV_H_
#include "mqtt5_client.h" #include "mqtt5_client.h"
#include "mqtt_client_priv.h"
#include "mqtt5_msg.h" #include "mqtt5_msg.h"
#ifdef __cplusplus #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); 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); 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); 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_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_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); esp_err_t esp_mqtt5_create_default_config(esp_mqtt5_client_handle_t client);
+14 -10
View File
@@ -10,6 +10,9 @@
static const char *TAG = "mqtt5_client"; 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 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, 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); 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) 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); client->send_publish_packet_count ++;
ESP_LOGD(TAG, "Sent (%d) qos > 0 publish packet without ack", client->send_publish_packet_count);
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);
}
} }
void esp_mqtt5_decrement_packet_counter(esp_mqtt5_client_handle_t client) 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; size_t len = client->mqtt_state.in_buffer_read_len;
client->mqtt_state.in_buffer_read_len = 0; client->mqtt_state.in_buffer_read_len = 0;
uint8_t ack_flag = 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. 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, 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.wildcard_subscribe_available = true;
client->mqtt5_config->server_resp_property_info.subscribe_identifiers_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.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; 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; return ESP_FAIL;
} }
/* Flow control to check PUBLISH(No PUBACK or PUBCOMP received) packet sent count(Only record QoS1 and QoS2)*/ return ESP_OK;
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",
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); client->mqtt5_config->server_resp_property_info.receive_maximum);
return ESP_FAIL; return ESP_FAIL;
} }
+105 -42
View File
@@ -843,6 +843,43 @@ static inline esp_err_t esp_mqtt_write(esp_mqtt_client_handle_t client)
return ESP_OK; 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) static esp_err_t esp_mqtt_connect(esp_mqtt_client_handle_t client, int timeout_ms)
{ {
int read_len, connect_rsp_code = 0; 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) { if (esp_mqtt5_parse_connack(client, &connect_rsp_code) == ESP_OK) {
client->send_publish_packet_count = 0; client->send_publish_packet_count = 0;
mqtt_requeue_transmitted_messages(client);
return ESP_OK; return ESP_OK;
} }
@@ -1690,14 +1728,14 @@ static esp_err_t mqtt_process_receive(esp_mqtt_client_handle_t client)
break; break;
case MQTT_MSG_TYPE_PUBACK: case MQTT_MSG_TYPE_PUBACK:
if (remove_initiator_message(client, MQTT_MSG_TYPE_PUBLISH, msg_id)) {
#ifdef MQTT_PROTOCOL_5 #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 #endif
if (remove_initiator_message(client, MQTT_MSG_TYPE_PUBLISH, msg_id)) {
ESP_LOGD(TAG, "received MQTT_MSG_TYPE_PUBACK, finish QoS1 publish"); ESP_LOGD(TAG, "received MQTT_MSG_TYPE_PUBACK, finish QoS1 publish");
#ifdef MQTT_PROTOCOL_5 #ifdef MQTT_PROTOCOL_5
esp_mqtt5_parse_puback(client); 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: case MQTT_MSG_TYPE_PUBCOMP:
ESP_LOGD(TAG, "received 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)) { 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"); ESP_LOGD(TAG, "Receive MQTT_MSG_TYPE_PUBCOMP, finish QoS2 publish");
#ifdef MQTT_PROTOCOL_5 #ifdef MQTT_PROTOCOL_5
esp_mqtt5_parse_pubcomp(client); 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; 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) 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, 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 // resend all non-transmitted messages first
outbox_item_handle_t item = outbox_dequeue(client->outbox, QUEUED, NULL); 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 (item) {
if (mqtt_resend_queued(client, item) == ESP_OK) { 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) { if (outbox_delete_item(client->outbox, item) != ESP_OK) {
ESP_LOGE(TAG, "Failed to remove queued qos0 message from the outbox"); ESP_LOGE(TAG, "Failed to remove queued qos0 message from the outbox");
} }
} } else {
outbox_set_tick(client->outbox, client->mqtt_state.pending_msg_id, platform_tick_get_ms());
if (client->mqtt_state.pending_publish_qos > 0 &&
mqtt_get_type(client->mqtt_state.connection.outbound_message.data) == MQTT_MSG_TYPE_PUBLISH) {
outbox_set_pending(client->outbox, client->mqtt_state.pending_msg_id, TRANSMITTED); outbox_set_pending(client->outbox, client->mqtt_state.pending_msg_id, TRANSMITTED);
#ifdef MQTT_PROTOCOL_5 #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); 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); item = outbox_dequeue(client->outbox, TRANSMITTED, &msg_tick);
if (item && (last_retransmit - msg_tick > client->config->message_retransmit_timeout)) { if (item && (last_retransmit - msg_tick > client->config->message_retransmit_timeout)) {
if (mqtt_resend_queued(client, item) == ESP_OK) { mqtt_resend_queued(client, item);
#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
}
} }
item = outbox_dequeue(client->outbox, ACKNOWLEDGED, &msg_tick); item = outbox_dequeue(client->outbox, ACKNOWLEDGED, &msg_tick);
if (item && (last_retransmit - msg_tick > client->config->message_retransmit_timeout)) { if (item && (last_retransmit - msg_tick > client->config->message_retransmit_timeout)) {
if (mqtt_resend_pubrel(client, item) == ESP_OK) { mqtt_resend_pubrel(client, item);
#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
}
} }
} }
@@ -2311,6 +2361,8 @@ int esp_mqtt_client_subscribe_multiple(esp_mqtt_client_handle_t client,
} }
MQTT_API_LOCK(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) { if (client->mqtt_state.connection.information.protocol_ver == MQTT_PROTOCOL_V_5) {
#ifdef MQTT_PROTOCOL_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; 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 */ /* Provide support for sending fragmented message if it doesn't fit buffer */
int remaining_len = len; int remaining_len = len;
const char *current_data = data; const char *current_data = data;
+77 -48
View File
@@ -17,7 +17,7 @@ This app exposes a console API for pytest-embedded HIL tests that target MQTT co
## JSON config keys ## 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`) ### `mqtt_config` (used with `init`)
@@ -25,63 +25,67 @@ Mirrors `esp_mqtt_client_config_t`'s nesting:
```json ```json
{ {
"mqtt_config": { "mqtt_config": {
"broker": { "address": { "uri": "mqtt://192.168.1.1:1883" } }, "broker": { "address": { "uri": "mqtt://192.168.1.1:1883" } },
"credentials": { "client_id": "my-client" }, "credentials": { "client_id": "my-client" },
"session": { "keepalive": 30, "disable_clean_session": false, "protocol_ver": 3 }, "session": {
"network": { "disable_auto_reconnect": true } "keepalive": 30,
} "disable_clean_session": false,
"protocol_ver": 3
},
"network": { "disable_auto_reconnect": true }
}
} }
``` ```
| Path | Type | Description | | Path | Type | Description |
|------|------|-------------| | -------------------------------- | ------ | ------------------------------------------------------------------------------------------------ |
| `broker.address.uri` | string | Broker URI (e.g. `mqtt://192.168.1.1:1883`) | | `broker.address.uri` | string | Broker URI (e.g. `mqtt://192.168.1.1:1883`) |
| `credentials.client_id` | string | Client identifier | | `credentials.client_id` | string | Client identifier |
| `session.keepalive` | int | Keepalive interval (seconds) | | `session.keepalive` | int | Keepalive interval (seconds) |
| `session.disable_clean_session` | bool | `true` = persistent session (clean start = false) | | `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 | | `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 | | `network.disable_auto_reconnect` | bool | Disable MQTT client automatic reconnect |
### `connect_property` (MQTT5 connect properties) ### `connect_property` (MQTT5 connect properties)
Already flat in C, so the JSON object is flat too: Already flat in C, so the JSON object is flat too:
| Key | Type | Description | | Key | Type | Description |
|-----|------|-------------| | ------------------------- | ---- | ----------------------------- |
| `session_expiry_interval` | int | Session expiry (seconds) | | `session_expiry_interval` | int | Session expiry (seconds) |
| `receive_maximum` | int | Receive maximum | | `receive_maximum` | int | Receive maximum |
| `topic_alias_maximum` | int | Topic alias maximum | | `topic_alias_maximum` | int | Topic alias maximum |
| `maximum_packet_size` | int | Maximum packet size | | `maximum_packet_size` | int | Maximum packet size |
| `will_delay_interval` | int | Will delay interval (seconds) | | `will_delay_interval` | int | Will delay interval (seconds) |
### `publish_property` (MQTT5 publish properties) ### `publish_property` (MQTT5 publish properties)
| Key | Type | Description | | Key | Type | Description |
|-----|------|-------------| | -------------------------- | ------ | ------------------------------ |
| `message_expiry_interval` | int | Message expiry (seconds) | | `message_expiry_interval` | int | Message expiry (seconds) |
| `payload_format_indicator` | bool | `true` = UTF-8 encoded payload | | `payload_format_indicator` | bool | `true` = UTF-8 encoded payload |
| `topic_alias` | int | Topic alias | | `topic_alias` | int | Topic alias |
| `content_type` | string | Content type | | `content_type` | string | Content type |
| `response_topic` | string | Response topic | | `response_topic` | string | Response topic |
### `subscribe_property` (MQTT5 subscribe properties) ### `subscribe_property` (MQTT5 subscribe properties)
| Key | Type | Description | | Key | Type | Description |
|-----|------|-------------| | -------------------------- | ------ | ------------------------------ |
| `subscribe_id` | int | Subscription identifier | | `subscribe_id` | int | Subscription identifier |
| `no_local_flag` | bool | No local flag | | `no_local_flag` | bool | No local flag |
| `retain_as_published_flag` | bool | Retain as published flag | | `retain_as_published_flag` | bool | Retain as published flag |
| `retain_handle` | int | Retain handling option (0/1/2) | | `retain_handle` | int | Retain handling option (0/1/2) |
| `is_share_subscribe` | bool | Shared subscription flag | | `is_share_subscribe` | bool | Shared subscription flag |
| `share_name` | string | Shared subscription group name | | `share_name` | string | Shared subscription group name |
### `disconnect_property` (MQTT5 disconnect properties) ### `disconnect_property` (MQTT5 disconnect properties)
| Key | Type | Description | | Key | Type | Description |
|-----|------|-------------| | ------------------------- | ---- | ------------------------------------- |
| `session_expiry_interval` | int | Session expiry override on disconnect | | `session_expiry_interval` | int | Session expiry override on disconnect |
| `disconnect_reason` | int | Disconnect reason code | | `disconnect_reason` | int | Disconnect reason code |
## Conformance mapping ## 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). 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: 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): 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 ```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. ```
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) 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] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version()); ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
esp_log_level_set("*", ESP_LOG_INFO); esp_log_level_set("*", ESP_LOG_INFO);
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 REQUIRES cmock mqtt esp_timer esp_hw_support http_parser log
WHOLE_ARCHIVE) 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;
}
+25
View File
@@ -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
View File
@@ -1,4 +1,5 @@
CONFIG_IDF_TARGET="linux" CONFIG_IDF_TARGET="linux"
CONFIG_MQTT_PROTOCOL_5=y
CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y
CONFIG_COMPILER_CXX_EXCEPTIONS=y CONFIG_COMPILER_CXX_EXCEPTIONS=y
CONFIG_COMPILER_CXX_RTTI=y CONFIG_COMPILER_CXX_RTTI=y
@@ -204,6 +204,31 @@ TEST_CASE("Outbox lookup by msg_id")
outbox_enqueue(outbox.handle, &message, 0); outbox_enqueue(outbox.handle, &message, 0);
REQUIRE(outbox_get(outbox.handle, 999) == nullptr); 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") TEST_CASE("Outbox delete by msg_id and type")