feat: Adds a state to reported states to differentiate init

Fixes #307
This commit is contained in:
Euripedes Rocha Filho
2026-08-17 12:14:33 +02:00
parent 9eac91a2ea
commit 078081edcc
9 changed files with 185 additions and 25 deletions
+14
View File
@@ -308,6 +308,20 @@ This function is used only for forcing the reconnect request, if you have an act
To disconnect from the broker use `esp_mqtt_client_disconnect`. It will perform a clean disconnect and if MQTT 5 is used and the client is configured to will send a disconnect message.
Querying client state
---------------------
Use :cpp:func:`esp_mqtt_client_get_state` to read the current lifecycle of a client handle. Typical values:
* ``MQTT_CLIENT_STATE_NOT_INITIALIZED`` — the handle is ``NULL``.
* ``MQTT_CLIENT_STATE_NOT_STARTED`` — the client is initialized, but its task is not running (never started, or already stopped).
* ``MQTT_CLIENT_STATE_CONNECTING`` — the task is running and a connection attempt is in progress.
* ``MQTT_CLIENT_STATE_CONNECTED`` — the client is connected to the broker.
* ``MQTT_CLIENT_STATE_WAITING_RECONNECT`` — the client is waiting for the reconnect timeout (or a call to :cpp:func:`esp_mqtt_client_reconnect`).
* ``MQTT_CLIENT_STATE_DISCONNECTED`` — the task is still running, but the client is not connected (for example while ``stop`` is completing).
This query is independent of event handlers: applications that reconfigure the broker URI or run a watchdog can ask whether the client is stopped, connecting, connected, or waiting to reconnect without mirroring every event.
Events
------
The following events may be posted by the MQTT client:
+14
View File
@@ -206,6 +206,20 @@ flash cache 被禁用时无法访问外部内存。因此,当任务栈位于
任务分发的事件处理函数中运行的用户代码。
查询客户端状态
---------------------
使用 :cpp:func:`esp_mqtt_client_get_state` 可读取客户端句柄的当前生命周期。常见返回值如下:
* ``MQTT_CLIENT_STATE_NOT_INITIALIZED`` — 句柄为 ``NULL``
* ``MQTT_CLIENT_STATE_NOT_STARTED`` — 客户端已初始化,但其任务未运行(从未启动,或已经停止)。
* ``MQTT_CLIENT_STATE_CONNECTING`` — 任务正在运行,并且正在尝试连接。
* ``MQTT_CLIENT_STATE_CONNECTED`` — 客户端已连接到服务器。
* ``MQTT_CLIENT_STATE_WAITING_RECONNECT`` — 客户端正在等待重连超时(或调用 :cpp:func:`esp_mqtt_client_reconnect`)。
* ``MQTT_CLIENT_STATE_DISCONNECTED`` — 任务仍在运行,但客户端未连接到服务器(例如 ``stop`` 尚未完成时)。
该查询独立于事件处理函数:需要重新配置服务器 URI 或运行看门狗的应用,无需镜像全部事件即可判断客户端是已停止、正在连接、已连接,还是正在等待重连。
事件
------------
MQTT 客户端可能会发布以下事件:
+10 -6
View File
@@ -158,10 +158,11 @@ typedef enum esp_mqtt_protocol_ver_t {
*/
typedef enum esp_mqtt_client_connection_state_t {
MQTT_CLIENT_STATE_NOT_INITIALIZED = 0, /*!< MQTT Client is not initialized */
MQTT_CLIENT_STATE_NOT_STARTED, /*!< MQTT Client is initialized, but not started */
MQTT_CLIENT_STATE_DISCONNECTED, /*!< MQTT Client is started, but not connected to the broker */
MQTT_CLIENT_STATE_NOT_STARTED, /*!< MQTT Client is initialized, but the client task is not running */
MQTT_CLIENT_STATE_DISCONNECTED, /*!< MQTT Client task is running, but not connected to the broker */
MQTT_CLIENT_STATE_CONNECTED, /*!< MQTT Client is connected to the broker */
MQTT_CLIENT_STATE_WAITING_RECONNECT, /*!< MQTT Client is waiting for reconnection request */
MQTT_CLIENT_STATE_CONNECTING, /*!< MQTT Client is connecting to the broker */
} esp_mqtt_client_connection_state_t;
/**
@@ -750,13 +751,16 @@ esp_transport_handle_t esp_mqtt_client_get_transport(esp_mqtt_client_handle_t cl
/**
* @brief Get MQTT client's current state
*
* Get the current state of MQTT client. Returns a value to indicate whether is it initialized, connected, waiting for
* reconnection, or disconnected.
* Get the current state of MQTT client.
*
* @param client *MQTT* client handle
* @return
* - MQTT client state on success
* - MQTT_CLIENT_STATE_INVALID on error
* - MQTT_CLIENT_STATE_NOT_INITIALIZED if client handle is NULL
* - MQTT_CLIENT_STATE_NOT_STARTED if the client task is not running
* - MQTT_CLIENT_STATE_CONNECTING if connecting to the broker
* - MQTT_CLIENT_STATE_DISCONNECTED if started but not connected to the broker
* - MQTT_CLIENT_STATE_CONNECTED if connected to the broker
* - MQTT_CLIENT_STATE_WAITING_RECONNECT if waiting for the reconnection timeout
*/
esp_mqtt_client_connection_state_t esp_mqtt_client_get_state(esp_mqtt_client_handle_t client);
+1
View File
@@ -135,6 +135,7 @@ struct esp_mqtt_client {
EventGroupHandle_t status_bits;
SemaphoreHandle_t api_lock;
TaskHandle_t task_handle;
atomic_bool task_running;
#if MQTT_EVENT_QUEUE_SIZE > 1
atomic_int queued_events;
#endif
+24 -19
View File
@@ -1041,6 +1041,7 @@ esp_mqtt_client_handle_t esp_mqtt_client_init(const esp_mqtt_client_config_t *co
goto _mqtt_init_failed;
}
atomic_init(&client->task_running, false);
#ifdef MQTT_SUPPORTED_FEATURE_EVENT_LOOP
esp_event_loop_args_t no_task_loop = {
.queue_size = MQTT_EVENT_QUEUE_SIZE,
@@ -2184,7 +2185,11 @@ static void esp_mqtt_task(void *pv)
esp_transport_close(client->transport);
outbox_delete_all_items(client->outbox);
MQTT_API_LOCK(client);
client->state = MQTT_STATE_DISCONNECTED;
client->task_handle = NULL;
atomic_store(&client->task_running, false);
MQTT_API_UNLOCK(client);
xEventGroupSetBits(client->status_bits, STOPPED_BIT);
#if MQTT_TASK_STACK_ON_EXTERNAL_MEMORY
vTaskDeleteWithCaps(NULL);
@@ -2209,6 +2214,7 @@ esp_err_t esp_mqtt_client_start(esp_mqtt_client_handle_t client)
}
esp_err_t err = ESP_OK;
client->state = MQTT_STATE_INIT;
#if MQTT_CORE_SELECTION_ENABLED
ESP_LOGD(TAG, "Core selection enabled on %u", MQTT_TASK_CORE);
#else
@@ -2232,6 +2238,7 @@ esp_err_t esp_mqtt_client_start(esp_mqtt_client_handle_t client)
}
#endif
atomic_store(&client->task_running, err == ESP_OK);
MQTT_API_UNLOCK(client);
return err;
}
@@ -2851,31 +2858,29 @@ esp_mqtt_client_connection_state_t esp_mqtt_client_get_state(esp_mqtt_client_han
return MQTT_CLIENT_STATE_NOT_INITIALIZED;
}
if (client->task_handle == NULL) {
return MQTT_CLIENT_STATE_NOT_STARTED;
}
esp_mqtt_client_connection_state_t ret = MQTT_CLIENT_STATE_DISCONNECTED;
MQTT_API_LOCK(client);
esp_mqtt_client_connection_state_t ret = MQTT_CLIENT_STATE_NOT_INITIALIZED;
switch (client->state) {
case MQTT_STATE_INIT:
if (!atomic_load(&client->task_running)) {
ret = MQTT_CLIENT_STATE_NOT_STARTED;
break;
} else {
switch (atomic_load(&client->state)) {
case MQTT_STATE_INIT:
ret = MQTT_CLIENT_STATE_CONNECTING;
break;
case MQTT_STATE_CONNECTED:
ret = MQTT_CLIENT_STATE_CONNECTED;
break;
case MQTT_STATE_CONNECTED:
ret = MQTT_CLIENT_STATE_CONNECTED;
break;
case MQTT_STATE_WAIT_RECONNECT:
ret = MQTT_CLIENT_STATE_WAITING_RECONNECT;
break;
case MQTT_STATE_WAIT_RECONNECT:
ret = MQTT_CLIENT_STATE_WAITING_RECONNECT;
break;
case MQTT_STATE_DISCONNECTED:
ret = MQTT_CLIENT_STATE_DISCONNECTED;
break;
case MQTT_STATE_DISCONNECTED:
ret = MQTT_CLIENT_STATE_DISCONNECTED;
break;
}
}
MQTT_API_UNLOCK(client);
return ret;
}
+1
View File
@@ -8,6 +8,7 @@ This app exposes a console API for pytest-embedded HIL tests that target MQTT co
- `config <base64_json>`: Apply base64-encoded JSON config to initialized client
- `start`: Start MQTT client
- `stop`: Stop MQTT client
- `get_state`: Print the current MQTT client connection state
- `disconnect`: Request disconnect
- `reconnect`: Request reconnect
- `destroy`: Destroy MQTT client
@@ -145,6 +145,17 @@ int do_stop(int argc, char **argv)
return 0;
}
int do_get_state(int argc, char **argv)
{
if (!client_available()) {
return 1;
}
auto state = esp_mqtt_client_get_state(command_context.mqtt_client);
ESP_LOGI(TAG, "CLIENT_STATE=%d", static_cast<int>(state));
return 0;
}
int do_disconnect(int argc, char **argv)
{
(void)argc;
@@ -328,6 +339,15 @@ void register_commands()
.func_w_context = nullptr,
.context = nullptr,
};
const esp_console_cmd_t get_state = {
.command = "get_state",
.help = "Print current MQTT client connection state as CLIENT_STATE=<N>",
.hint = nullptr,
.func = &do_get_state,
.argtable = nullptr,
.func_w_context = nullptr,
.context = nullptr,
};
const esp_console_cmd_t disconnect = {
.command = "disconnect",
.help = "Disconnect mqtt client",
@@ -398,6 +418,7 @@ void register_commands()
ESP_ERROR_CHECK(esp_console_cmd_register(&config_cmd));
ESP_ERROR_CHECK(esp_console_cmd_register(&start));
ESP_ERROR_CHECK(esp_console_cmd_register(&stop));
ESP_ERROR_CHECK(esp_console_cmd_register(&get_state));
ESP_ERROR_CHECK(esp_console_cmd_register(&disconnect));
ESP_ERROR_CHECK(esp_console_cmd_register(&reconnect));
ESP_ERROR_CHECK(esp_console_cmd_register(&destroy));
@@ -413,6 +413,19 @@ def started_client(dut: Dut) -> Generator[Dut, None, None]:
stop_client(dut)
# esp_mqtt_client_connection_state_t integer values
MQTT_CLIENT_STATE_NOT_STARTED = 1
MQTT_CLIENT_STATE_CONNECTED = 3
MQTT_CLIENT_STATE_WAITING_RECONNECT = 4
MQTT_CLIENT_STATE_CONNECTING = 5
def assert_client_state(dut: Dut, expected: int) -> None:
"""Issue get_state and assert the DUT reports the expected numeric state."""
dut.write("get_state")
dut.expect(re.compile(f"CLIENT_STATE={expected}(?![0-9])".encode()), timeout=DUT_CMD_TIMEOUT)
def case_timeout(
*,
connect_operations: int = 0,
@@ -500,6 +513,46 @@ def expect_n(
return seen
@pytest.mark.eth_ip101
@pytest.mark.timeout(
case_timeout(
connect_operations=1,
event_wait_operations=1,
)
)
@idf_parametrize("target", ["esp32"], indirect=["target"])
def test_client_state_transitions(dut: Dut) -> None:
"""
Verify esp_mqtt_client_get_state across the observable lifecycle:
NOT_STARTED → CONNECTING → CONNECTED → WAITING_RECONNECT → NOT_STARTED.
Holding CONNACK makes the otherwise short CONNECTING state deterministic.
"""
with (
broker_started(hold_packet_types=(MqttPacketType.CONNACK,)) as broker,
initialized_mqtt_client(dut, broker.uri) as client,
):
assert_client_state(client, MQTT_CLIENT_STATE_NOT_STARTED)
client.write("start")
try:
broker.wait_for_held_packets(MqttPacketType.CONNACK, 1, timeout=DUT_CONNECT_TIMEOUT)
assert_client_state(client, MQTT_CLIENT_STATE_CONNECTING)
finally:
broker.release_held_packets(MqttPacketType.CONNACK)
client.expect(re.compile(rb"MQTT_EVENT_CONNECTED"), timeout=DUT_CONNECT_TIMEOUT)
assert_client_state(client, MQTT_CLIENT_STATE_CONNECTED)
client.write("disconnect")
client.expect(re.compile(rb"MQTT_EVENT_DISCONNECTED"), timeout=DUT_EVENT_TIMEOUT)
assert_client_state(client, MQTT_CLIENT_STATE_WAITING_RECONNECT)
client.write("stop")
client.expect(re.compile(rb"Mqtt client stopped"), timeout=DUT_CMD_TIMEOUT)
assert_client_state(client, MQTT_CLIENT_STATE_NOT_STARTED)
@pytest.mark.eth_ip101
@pytest.mark.timeout(
case_timeout(
+47
View File
@@ -86,6 +86,22 @@ using unique_mqtt_client =
esp_mqtt_client_destroy(client);
}) >;
static BaseType_t create_fake_task(TaskFunction_t, const char *const, const uint32_t,
void *const, UBaseType_t, TaskHandle_t *const created_task,
const BaseType_t, int)
{
static int fake_task_storage;
*created_task = reinterpret_cast<TaskHandle_t>(&fake_task_storage);
return pdTRUE;
}
static BaseType_t fail_to_create_fake_task(TaskFunction_t, const char *const, const uint32_t,
void *const, UBaseType_t, TaskHandle_t *const,
const BaseType_t, int)
{
return pdFALSE;
}
SCENARIO("MQTT Client Operation")
{
// Set expectations for the mocked calls.
@@ -319,6 +335,37 @@ SCENARIO("MQTT Client Operation")
// Only need to start the client, destroy is called automatically at the
// end of scope
}
SECTION("get_state reports client lifecycle correctly") {
SECTION("returns NOT_INITIALIZED for null handle") {
REQUIRE(esp_mqtt_client_get_state(nullptr) ==
MQTT_CLIENT_STATE_NOT_INITIALIZED);
}
SECTION("returns NOT_STARTED before esp_mqtt_client_start is called") {
REQUIRE(esp_mqtt_client_get_state(client.get()) ==
MQTT_CLIENT_STATE_NOT_STARTED);
}
SECTION("returns CONNECTING after start, not NOT_STARTED") {
/* After start the task handle is non-NULL but the task never
* runs in the mock environment, so client->state stays at
* MQTT_STATE_INIT (=0 from calloc). This is precisely the
* condition that was buggy: get_state must return
* MQTT_CLIENT_STATE_CONNECTING, not MQTT_CLIENT_STATE_NOT_STARTED.
*
* Use Stub (not ExpectAnyArgs) so the fake task handle is set
* regardless of any stale expectations queued by sibling sections. */
xTaskCreatePinnedToCore_Stub(create_fake_task);
REQUIRE(esp_mqtt_client_start(client.get()) == ESP_OK);
auto state = esp_mqtt_client_get_state(client.get());
REQUIRE(state == MQTT_CLIENT_STATE_CONNECTING);
REQUIRE(state != MQTT_CLIENT_STATE_NOT_STARTED);
}
SECTION("returns NOT_STARTED when task creation fails") {
xTaskCreatePinnedToCore_Stub(fail_to_create_fake_task);
REQUIRE(esp_mqtt_client_start(client.get()) == ESP_FAIL);
REQUIRE(esp_mqtt_client_get_state(client.get()) ==
MQTT_CLIENT_STATE_NOT_STARTED);
}
}
}
SECTION("Client with all allocating configuration set") {
xQueueCreateMutex_IgnoreAndReturn(