Chapter 107: MQTT Last Will and Testament
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the purpose and functionality of the MQTT Last Will and Testament (LWT) feature.
- Identify the conditions under which an LWT message is published by the MQTT broker.
- Configure LWT parameters—topic, message, Quality of Service (QoS), and retain flag—within an ESP-IDF project.
- Implement LWT in an ESP32 application to reliably signal unexpected disconnections.
- Utilize LWT messages to monitor device presence and online/offline status.
- Follow best practices for designing LWT topics and crafting LWT messages.
- Recognize that LWT functionality is consistent across different ESP32 variants.
Introduction
In the preceding chapters, we’ve delved into the MQTT protocol, covering client implementation, Quality of Service levels, security, and topic design. A fundamental challenge in any distributed IoT system is knowing the status of remote devices. Is a sensor still online? Did a critical actuator suddenly drop off the network due to a power failure or a software crash? Relying solely on the absence of regular messages can be slow and ambiguous.
The MQTT protocol provides an elegant solution to this problem: the Last Will and Testament (LWT). This feature allows an MQTT client to inform the broker of a message (the “will”) that should be published on its behalf if the client disconnects ungracefully. This chapter will explore the theory behind LWT, demonstrate its practical implementation on ESP32 devices using ESP-IDF, and discuss best practices for its use in robust IoT applications.
Theory
What is a Last Will and Testament (LWT)?
The Last Will and Testament (LWT) is a feature in MQTT that allows a client to specify a message that the MQTT broker should publish on a designated topic if the client disconnects from the broker unexpectedly (i.e., without sending a proper DISCONNECT
packet). Think of it as a “final message” that the device wishes to convey if it suddenly “dies” or vanishes from the network.
When an MQTT client connects to a broker, it can optionally provide these LWT parameters:
- Will Topic: The specific MQTT topic to which the LWT message will be published.
- Will Message (Payload): The actual content of the message to be published (e.g., a simple string like “offline”, or a JSON object like
{"status": "offline", "deviceId": "sensor01"}
). - Will QoS: The Quality of Service level (0, 1, or 2) for the LWT message. This determines the reliability of the LWT message delivery from the broker to subscribers of the Will Topic.
- Will Retain Flag: A boolean flag. If set to
true
, the LWT message published by the broker will be retained on the Will Topic. This means any new client subscribing to the Will Topic will immediately receive the last LWT message, even if it was published some time ago.
The broker stores these LWT parameters for the duration of the client’s connection.
LWT Parameter | Description | ESP-IDF Field (in .session.last_will ) |
Example Value |
---|---|---|---|
Will Topic | The MQTT topic where the LWT message will be published by the broker if the client disconnects ungracefully. | .topic |
devices/esp32_sensor_A/status |
Will Message (Payload) | The actual content of the LWT message to be published. Can be a simple string or structured data like JSON. | .msg |
{"state": "offline"} or "Device_X_Disconnected" |
Will Message Length | Length of the Will Message. If set to 0, the library uses strlen() on the .msg . Must be set accurately if message contains null bytes. |
.msg_len |
0 (for null-terminated strings) or actual length. |
Will QoS | The Quality of Service level (0, 1, or 2) for publishing the LWT message from the broker to subscribers. | .qos |
1 (At least once – common choice) |
Will Retain Flag | A boolean flag. If true, the LWT message published by the broker is retained on the Will Topic. | .retain |
true (Useful for status topics) |
When is the LWT Message Published?
The MQTT broker publishes the LWT message under specific conditions that indicate an ungraceful or abnormal disconnection of the client. These include:
- Network Failure: The underlying TCP/IP connection between the client and broker is broken (e.g., network cable unplugged, Wi-Fi access point goes down, client loses power abruptly).
- Keep-Alive Timeout: The client fails to send any control packet (including
PINGREQ
if no other data is flowing) within the Keep Alive period negotiated during connection. The broker then assumes the client is no longer responsive and closes the connection. - Client Protocol Violation: The client sends a malformed packet, causing the broker to close the connection.
- Server-Side Disconnection: The broker itself might disconnect the client for administrative reasons, and if it deems the disconnection ungraceful from the client’s perspective, it might publish the LWT.
Crucially, the LWT message is NOT published if the client performs a graceful disconnection by sending a DISCONNECT
packet to the broker. This is because a graceful disconnect implies the client is intentionally closing the connection and can manage its status updates through regular MQTT messages if needed.
Scenario / Condition | Broker Action Regarding LWT | Reasoning |
---|---|---|
Network Failure (TCP/IP connection broken) | Publishes LWT | Client disappeared without proper notification; ungraceful disconnect. |
Client Loses Power Abruptly | Publishes LWT | Similar to network failure; TCP connection drops or keep-alive will eventually time out. |
Keep-Alive Timeout | Publishes LWT | Client failed to send PINGREQ or any other packet within the negotiated keep-alive interval * 1.5 (typically). Broker assumes client is dead. |
Client Protocol Violation (Malformed Packet) | Publishes LWT | Broker closes connection due to client error, considered an ungraceful client exit. |
Server-Side Disconnection (Administrative, deemed ungraceful by broker for client) | May Publish LWT | Broker-dependent, but if the broker closes the session in a way that doesn’t allow the client to send a DISCONNECT. |
Client Sends a DISCONNECT Packet | LWT NOT Published | Client performed a graceful, intentional disconnection. LWT is not needed. |
sequenceDiagram participant C as Client (ESP32) participant B as MQTT Broker participant M as Monitoring Client(s) C->>B: 1. CONNECT (Includes LWT Params) <br> Will Topic: devices/esp32_sensor_A/status <br> Will Message: {"state": "offline"} <br> Will QoS: 1, Will Retain: true B-->>C: 2. CONNACK (Connection Accepted) Note over B: Stores LWT for esp32_sensor_A C->>B: 3. PUBLISH (Optional "online" status) <br> Topic: devices/esp32_sensor_A/status <br> Payload: {"state": "online"} <br> QoS: 1, Retain: true B->>M: 4. Delivers "online" message (if M subscribed) loop Normal Operation C->>B: Publishes data to other topics C->>B: PINGREQ (Keep-Alive) B-->>C: PINGRESP end Note over C,B: --- Scenario: Ungraceful Disconnect --- <br> (ESP32 loses power / Wi-Fi drops / crashes / Keep-Alive timeout) B--xM: Detects Ungraceful Disconnect of C Note over B: Publishes Stored LWT Message B->>M: 5. PUBLISH (LWT Message) <br> Topic: devices/esp32_sensor_A/status <br> Payload: {"state": "offline"} <br> QoS: 1, Retain: true Note over M: Receives LWT message. <br> Knows esp32_sensor_A is offline. participant NM as New Monitoring Client NM->>B: 6. SUBSCRIBE to devices/esp32_sensor_A/status B->>NM: 7. Delivers Retained LWT Message <br> Payload: {"state": "offline"}
LWT Parameters in Detail
- Will Topic:
- This should be a standard MQTT topic string.
- It’s common practice to use a topic that clearly indicates it’s a status or presence topic, often including the device ID.
- Example:
devices/my_esp32_id/status
,presence/my_esp32_id
,system/nodes/my_esp32_id/online_state
.
- Will Message (Payload):
- The content can be anything the application requires.
- Simple strings like
"offline"
,"0"
, or"disconnected"
are common. - JSON payloads like
{"status": "offline", "timestamp": 1678886400}
can provide more context. - The message should be concise, as it’s meant to be a quick status indicator.
- Will QoS:
- QoS 0 (At most once): The broker will attempt to publish the LWT message once. It might get lost if network issues persist or if the subscribing client is temporarily disconnected. Generally not recommended for critical offline notifications.
- QoS 1 (At least once): The broker will ensure the LWT message is delivered to subscribers at least once. This is a common and recommended choice for LWT, providing a good balance of reliability and overhead. The message will be re-delivered until acknowledged by the subscriber.
- QoS 2 (Exactly once): The broker will ensure the LWT message is delivered exactly once using a four-part handshake. This provides the highest reliability but also has the most overhead. It might be overkill for simple offline status messages unless absolute certainty is required and the system is designed to handle the complexities of QoS 2.
- Will Retain Flag:
false
(Default for many clients, but check ESP-IDF defaults): The LWT message is published as a regular message. Only clients currently subscribed to the Will Topic at the moment of publication will receive it. If a monitoring client subscribes after the LWT was published, it won’t receive that past LWT message.true
: The LWT message is published as a retained message. The broker stores this message (as the last known “good” message for that topic). Any client that subscribes to the Will Topic, even later, will immediately receive this retained LWT message. This is very useful for device status, as a monitoring application can always get the last known state (e.g., “offline”) upon connecting or subscribing.
Parameter | Setting | Description & Implication for LWT | Recommendation |
---|---|---|---|
Will QoS | 0 (At most once) |
Broker attempts to publish LWT once. Delivery is not guaranteed; message might be lost. Lowest overhead. | Generally NOT recommended for critical offline notifications. |
1 (At least once) |
Broker ensures LWT message is delivered to subscribers at least once (requires ACK from subscriber). Good balance of reliability and overhead. | Common and recommended choice for LWT. | |
2 (Exactly once) |
Broker ensures LWT message is delivered exactly once using a four-part handshake. Highest reliability, highest overhead. | Potentially overkill for simple status; use if absolute certainty is required and system handles QoS 2 complexities. | |
Will Retain Flag | false |
LWT message is published as a regular, non-retained message. Only currently connected subscribers receive it. New subscribers later will not get this past LWT. | Use if only immediate notification to active listeners is needed, and historical status isn’t critical for new subscribers. |
true |
LWT message is published as a retained message. Broker stores it as the last message for that topic. New subscribers immediately receive this last LWT message. | Highly recommended for device status topics so monitoring apps can always get the last known state (e.g., “offline”). Remember to update with an “online” (retained) message on reconnect. |
Warning: If
Will Retain
istrue
, ensure your application logic handles this. For example, if the LWT message is “offline” and retained, the device, upon reconnecting, should ideally publish an “online” message to the same topic with the retain flag set to true to update the retained status. Otherwise, the “offline” message might persist as the retained message even when the device is back online.
Use Cases for LWT
- Device Presence Detection: The primary use case. Monitoring applications can subscribe to LWT topics to know which devices are currently online or have gone offline unexpectedly.
- Status Monitoring: LWT messages can indicate the device’s last known state before an ungraceful shutdown.
- Triggering Alerts: An “offline” LWT message can trigger alerts (e.g., email, SMS, dashboard notification) to inform administrators of a potential issue with a device.
- Automated Recovery Actions: In more advanced systems, an LWT message might trigger automated attempts to diagnose or recover the offline device (though this is typically handled by a separate management system).
- Graceful Degradation: Services relying on data from a device can use its LWT status to gracefully degrade functionality if the device goes offline.
LWT vs. Keep-Alive
It’s important to distinguish LWT from the MQTT Keep-Alive mechanism:
- Keep-Alive: This is a timer negotiated between the client and broker. If the client has no other messages to send, it must send a
PINGREQ
packet within the Keep-Alive interval. If the broker doesn’t receive any communication (includingPINGREQ
) from the client within typically 1.5 times the Keep-Alive interval, it considers the client disconnected. Keep-Alive is the detection mechanism for a dead or unresponsive client. - LWT: This is the action the broker takes (publishing the Will Message) after it has detected an ungraceful disconnection, often as a result of a Keep-Alive timeout or a sudden TCP connection drop.
Feature | MQTT Keep-Alive | MQTT Last Will and Testament (LWT) |
---|---|---|
Primary Purpose | Mechanism for the broker to detect if a client has become unresponsive or disconnected without a formal DISCONNECT packet. | A message pre-defined by the client that the broker publishes on the client’s behalf if it disconnects ungracefully. |
Who Initiates/Configures? | Client proposes a Keep-Alive interval during CONNECT; broker may accept or adjust. Client is responsible for sending PINGREQ if no other data is flowing. | Client configures LWT parameters (topic, message, QoS, retain) during the CONNECT packet. |
Action Trigger | Broker detects no communication (including PINGREQ) from client within ~1.5 times the agreed Keep-Alive interval. | Broker detects an ungraceful client disconnection (often due to Keep-Alive timeout, TCP drop, or client protocol error). |
Resulting Action by Broker | Broker considers the client disconnected and closes the network connection. | Broker publishes the pre-configured Will Message to the Will Topic. |
Communication Direction | Client → Broker (PINGREQ/data packets) Broker → Client (PINGRESP) |
Client → Broker (LWT parameters at CONNECT time) Broker → Subscribers of Will Topic (LWT message publication) |
Analogy | A regular “heartbeat” or “check-in” to show the client is still alive and connected. | A “final note” or “instructions” left behind by the client in case of sudden demise. |
Relationship | They work together: Keep-Alive helps the broker detect an ungraceful disconnection. LWT provides a mechanism to notify other clients about this event. |
They work together: Keep-Alive helps the broker detect the problem, and LWT helps inform other interested parties about the problem.
Practical Examples
Let’s implement LWT for an ESP32 device. The device will configure an LWT message indicating it’s “offline”. For better status tracking, upon successful connection, it will also publish an “online” message to the same topic, ensuring the retained message reflects its current state.
Scenario:
An ESP32 device connects to an MQTT broker.
- LWT Configuration:
- Will Topic:
smartdevice/esp32_office_01/status
- Will Message:
{"state": "offline", "reason": "unspecified_disconnect"}
- Will QoS: 1
- Will Retain:
true
- Will Topic:
- On Successful Connection:
- The ESP32 will publish to
smartdevice/esp32_office_01/status
- Message:
{"state": "online", "timestamp": <current_device_time>}
- QoS: 1
- Retain:
true
- The ESP32 will publish to
Assumed Setup:
- An MQTT broker is accessible (e.g., Mosquitto, HiveMQ Cloud,
test.mosquitto.org
). - MQTT client tool (e.g., MQTTX) for observation.
- ESP-IDF v5.x project environment.
Code Snippets (ESP-IDF v5.x)
This code demonstrates how to configure LWT in the esp_mqtt_client_config_t
structure.
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mqtt_client.h"
#include "cJSON.h" // For JSON payload creation
#include "esp_mac.h" // For device ID
static const char *TAG = "LWT_EXAMPLE";
// MQTT Configuration - REPLACE WITH YOUR BROKER DETAILS
#define MQTT_BROKER_URI "mqtt://test.mosquitto.org" // Example broker
#define DEVICE_BASE_TOPIC "smartdevice/" // Base for our topics
// Buffer for dynamic topic creation
char device_id_str[18]; // For MAC address as string
char lwt_topic_buf[128];
char online_payload_buf[128];
char offline_lwt_payload_buf[128];
static void log_error_if_nonzero(const char *message, int error_code) {
if (error_code != 0) {
ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code);
}
}
/*
* @brief Event handler registered to receive MQTT events
*
* This function is called by the MQTT client event loop.
*/
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%ld", base, event_id);
esp_mqtt_event_handle_t event = event_data;
esp_mqtt_client_handle_t client = event->client;
int msg_id;
switch ((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
// Publish "online" status message to the LWT topic with retain=true
// This overwrites the "offline" LWT message if it was previously retained
snprintf(online_payload_buf, sizeof(online_payload_buf),
"{\"state\": \"online\", \"device_id\": \"%s\", \"timestamp\": %lld}",
device_id_str, (long long)time(NULL)); // Using placeholder for time
msg_id = esp_mqtt_client_publish(client, lwt_topic_buf, online_payload_buf, 0, 1, 1); // QoS 1, Retain 1
ESP_LOGI(TAG, "Sent publish successful, msg_id=%d, topic=%s, data=%s", msg_id, lwt_topic_buf, online_payload_buf);
// Example: Subscribe to a command topic (not directly related to LWT but typical)
char cmd_topic_buf[128];
snprintf(cmd_topic_buf, sizeof(cmd_topic_buf), "%s%s/commands/#", DEVICE_BASE_TOPIC, device_id_str);
msg_id = esp_mqtt_client_subscribe(client, cmd_topic_buf, 0);
ESP_LOGI(TAG, "Sent subscribe successful, msg_id=%d to topic %s", msg_id, cmd_topic_buf);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
// LWT will be published by the broker if this was an ungraceful disconnect
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
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);
// Handle incoming commands if subscribed
break;
case MQTT_EVENT_ERROR:
ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
log_error_if_nonzero("reported from esp-tls", event->error_handle->esp_tls_last_esp_err);
log_error_if_nonzero("reported from tls stack", event->error_handle->esp_tls_stack_err);
log_error_if_nonzero("captured as transport's socket errno", event->error_handle->esp_transport_sock_errno);
ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno));
}
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->id);
break;
}
}
static void mqtt_app_start(void) {
// Generate a unique device ID (e.g., from MAC address)
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA); // Get STA MAC address
snprintf(device_id_str, sizeof(device_id_str), "%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// Construct LWT topic and payload
snprintf(lwt_topic_buf, sizeof(lwt_topic_buf), "%s%s/status", DEVICE_BASE_TOPIC, device_id_str);
snprintf(offline_lwt_payload_buf, sizeof(offline_lwt_payload_buf),
"{\"state\": \"offline\", \"device_id\": \"%s\", \"reason\": \"unspecified_disconnect\"}",
device_id_str);
ESP_LOGI(TAG, "Device ID: %s", device_id_str);
ESP_LOGI(TAG, "LWT Topic: %s", lwt_topic_buf);
ESP_LOGI(TAG, "LWT Offline Payload: %s", offline_lwt_payload_buf);
const esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = MQTT_BROKER_URI,
.credentials.client_id = device_id_str, // Use unique client ID
.session.last_will = {
.topic = lwt_topic_buf,
.msg = offline_lwt_payload_buf,
.msg_len = 0, // 0 means length is derived from strlen(msg)
.qos = 1,
.retain = true
},
// .network.disable_auto_reconnect = false, // Default is false (auto-reconnect enabled)
// .session.keepalive = 60 // Default is 120 seconds
};
ESP_LOGI(TAG, "Initializing MQTT client...");
esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
/* The last argument may be used to pass data to the event handler, in this example mqtt_event_handler */
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_mqtt_client_start(client);
}
// Dummy WiFi connection part (replace with your actual WiFi connection logic from previous chapters)
static void wifi_init_sta(void) {
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
// For brevity, direct event registration. In a real app, use a dedicated WiFi event handler.
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
NULL, // No specific handler for all wifi events here
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
NULL, // No specific handler for got_ip here
NULL,
&instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.ssid = "YOUR_WIFI_SSID", // REPLACE WITH YOUR WIFI SSID
.password = "YOUR_WIFI_PASSWORD", // REPLACE WITH YOUR WIFI PASSWORD
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
ESP_ERROR_CHECK(esp_wifi_start() );
ESP_LOGI(TAG, "wifi_init_sta finished.");
// For this example, we assume connection happens. In a real app, wait for IP_EVENT_STA_GOT_IP.
// This is simplified for focusing on MQTT LWT.
// A proper implementation would wait for the IP_EVENT_STA_GOT_IP event before starting MQTT.
vTaskDelay(pdMS_TO_TICKS(5000)); // Crude delay to allow WiFi to connect
ESP_LOGI(TAG, "Proceeding to start MQTT app assuming WiFi is connected.");
}
void app_main(void) {
ESP_LOGI(TAG, "[APP] Startup..");
ESP_LOGI(TAG, "[APP] Free memory: %lu bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
esp_log_level_set("*", ESP_LOG_INFO);
esp_log_level_set("mqtt_client", ESP_LOG_VERBOSE);
esp_log_level_set("MQTT_EXAMPLE", ESP_LOG_VERBOSE); // Our main tag
esp_log_level_set("transport_base", ESP_LOG_VERBOSE);
esp_log_level_set("esp-tls", ESP_LOG_VERBOSE);
esp_log_level_set("transport", ESP_LOG_VERBOSE);
esp_log_level_set("outbox", ESP_LOG_VERBOSE);
ESP_ERROR_CHECK(nvs_flash_init());
// ESP_ERROR_CHECK(esp_netif_init()); // Done in wifi_init_sta
// ESP_ERROR_CHECK(esp_event_loop_create_default()); // Done in wifi_init_sta
// Initialize WiFi (replace with your robust WiFi connection logic)
wifi_init_sta();
// Start MQTT
mqtt_app_start();
}
Tip:
- Ensure
CONFIG_BROKER_URL
is set in yoursdkconfig
if you useCONFIG_BROKER_URL
inmqtt_cfg
, or directly assign the URI string as shown. - The
msg_len = 0
forlast_will.msg
tells the MQTT client to usestrlen()
on themsg
string. If your LWT message contains null bytes, you must specify the correctmsg_len
. - Add
cJSON
andesp_wifi
to your component’sCMakeLists.txt
if not already present:# In main/CMakeLists.txt
# ...
REQUIRES esp_wifi cJSON esp_event
# ...
EnsurecJSON
is added as a dependency viaidf.py add-dependency "espressif/cjson^1.7.15"
or similar.
Build Instructions
- Save the code: Place the C code into your project’s
main/main.c
file. - Configure WiFi: Update
"YOUR_WIFI_SSID"
and"YOUR_WIFI_PASSWORD"
inwifi_init_sta()
. - Configure MQTT Broker: Update
MQTT_BROKER_URI
if you are not usingtest.mosquitto.org
. - Build the project:
idf.py build
Run/Flash/Observe Steps
- Flash the firmware:
idf.py -p /dev/ttyUSB0 flash monitor
(Replace/dev/ttyUSB0
with your ESP32’s serial port). - Prepare MQTT Client Tool (e.g., MQTTX):
- Connect your MQTT client tool to the same broker used by the ESP32.
- Subscribe to the LWT topic. Since the device ID (MAC address) is dynamic, you might first run the ESP32 to see its MAC address in the logs, then construct the topic:
smartdevice/YOUR_ESP32_MAC_ADDRESS/status
. - Alternatively, subscribe with a wildcard to see all status messages:
smartdevice/+/status
.
- Observe Normal Operation:
- When the ESP32 connects to WiFi and then to the MQTT broker, it will log “MQTT_EVENT_CONNECTED”.
- Immediately after, it will publish the “online” message (
{"state": "online", ...}
) to its status topic (smartdevice/YOUR_ESP32_MAC_ADDRESS/status
) with retain set totrue
. - In MQTT Explorer, you should see this “online” message. Because it’s retained, if you disconnect and reconnect MQTT Explorer (or a new client subscribes), it will immediately receive this “online” message.
- Simulate an Ungraceful Disconnect:
- While the ESP32 is running and connected, press the RESET button on the ESP32 board, or simply unplug its power. This simulates an ungraceful disconnection.
- Wait for a short period (depending on the broker’s Keep-Alive handling, usually 1.5 times the client’s keep-alive interval, default 120s for ESP-MQTT, so up to 180s, but often much faster if TCP connection breaks).
- Observe in MQTT Explorer: The broker will publish the LWT message (
{"state": "offline", ...}
) tosmartdevice/YOUR_ESP32_MAC_ADDRESS/status
. This message will also be retained, overwriting the previous “online” message.
- Observe Reconnection (Optional):
- If you power the ESP32 back on or let it recover from reset, it will reconnect.
- Upon reconnection, it will again publish the “online” message, which will update the retained message on the broker, correctly reflecting its current status.
Variant Notes
The MQTT Last Will and Testament is a standard feature of the MQTT protocol itself. The ESP-IDF esp-mqtt
client component implements this standard feature. Therefore, the configuration and behavior of LWT are identical across all ESP32 variants, including:
- ESP32
- ESP32-S2
- ESP32-S3
- ESP32-C3
- ESP32-C6
- ESP32-H2
Key considerations that are common to all variants:
- Device Identification: The method for generating a unique
device_id
(used incredentials.client_id
and often in the LWT topic) is consistent. Using the MAC address (esp_read_mac()
oresp_efuse_mac_get_default()
) is a common approach for all Wi-Fi enabled variants. For variants that might connect via other means (e.g., Ethernet on some ESP32s, or Thread/Zigbee on ESP32-H2 acting as a gateway), the principle of unique identification for the MQTT client remains. - LWT Configuration: The
esp_mqtt_client_config_t
structure and its.session.last_will
member are used uniformly. - Network Stack: While the underlying network interface (Wi-Fi, Ethernet, Thread) might differ, once a TCP/IP connection is established to the MQTT broker, the LWT mechanism operates at the MQTT protocol level, which is abstracted by the
esp-mqtt
client. - Causes of Disconnection: The specific hardware or software issues that might lead to an ungraceful disconnection could vary (e.g., a peripheral failure unique to one variant causing a crash). However, the LWT mechanism’s role is to report the outcome (the ungraceful disconnection), not diagnose the variant-specific cause.
In summary, when implementing LWT, you do not need to make variant-specific code changes related to the LWT feature itself. The provided examples and principles apply universally to any ESP32 chip running ESP-IDF v5.x.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom / Description | Solution / Fix |
---|---|---|
LWT Message Not Published | Device disconnects, but the expected LWT message doesn’t appear on the Will Topic. |
|
Misusing Retain Flag |
1. Will Retain = false : New subscribers don’t see the offline status.2. Will Retain = true for LWT (“offline”), but no retained “online” message published on reconnect.
|
|
Incorrect LWT QoS | Using QoS 0 for critical LWT messages, leading to potential loss of the offline notification. | Use Will QoS 1 (recommended) or QoS 2 for reliable LWT delivery from broker to subscribers. |
Complex LWT Messages/Topics | LWT topics are hard to subscribe to (e.g., too generic or inconsistent). LWT messages are too large or complex, increasing overhead. | Keep LWT topics clear and specific (e.g., devices/[deviceID]/status ). Keep LWT messages concise (e.g., "offline" , {"status":"offline"} ). |
Non-Unique Client ID | Multiple devices use the same Client ID. Broker behavior with LWTs can be unpredictable or only one LWT might be stored/triggered. | Ensure every MQTT client has a unique Client ID. Using the device MAC address for the Client ID is a common strategy. |
LWT Message Length (.msg_len ) |
LWT message contains null bytes, but .msg_len is set to 0 (relying on strlen ), causing truncated LWT message. |
If LWT message payload includes null characters (e.g., binary data), explicitly set .msg_len to the correct length of the payload. For simple C strings, 0 is fine. |
Exercises
- Granular Offline Reason:
- Task: Modify the practical example. Instead of a generic “unspecified_disconnect” in the LWT message, imagine your ESP32 application can detect a potential reason before a likely crash (e.g., critical sensor failure, low battery warning just before shutdown).
- Challenge: How would you dynamically update the LWT message held by the broker before the ungraceful disconnect occurs if the client is still connected but anticipates a problem? (Hint: MQTT 5.0 has features for this, but with MQTT 3.1.1, the client would need to disconnect and reconnect with new LWT parameters. Discuss the implications or simulate a simplified approach where the LWT message is generic, but the device tries to publish a more specific “going_offline_due_to_X” message just before it expects to lose connection, if possible).
- For this exercise, focus on designing different LWT payload structures for different offline reasons (e.g.,
{"state": "offline", "reason": "low_battery"}
,{"state": "offline", "reason": "sensor_fault"}
). Then, manually change theoffline_lwt_payload_buf
in the code before flashing to simulate these different LWTs and observe them.
- LWT-Powered Device Dashboard Indicator:
- Task: Create a very simple Python script (using the
paho-mqtt
library) that acts as a dashboard status indicator. - The script should:
- Connect to the MQTT broker.
- Subscribe to the LWT topic pattern used by your ESP32(s) (e.g.,
smartdevice/+/status
). - When it receives a message:
- Parse the JSON payload.
- Print a message like:
Device [device_id] is now [state] (Timestamp: [timestamp])
. - If
state
is “offline”, print an additional alert:ALERT: Device [device_id] has gone offline!
- Test: Run your ESP32 example. Observe the Python script output when the ESP32 connects (“online”) and when you simulate an ungraceful disconnect (“offline”).
- Task: Create a very simple Python script (using the
Summary
- The Last Will and Testament (LWT) is an MQTT feature where a client pre-registers a message with the broker to be published if the client disconnects ungracefully.
- LWT is crucial for device presence detection and monitoring online/offline status.
- The LWT message is published by the broker only upon abnormal client disconnections (e.g., TCP drop, keep-alive timeout), not on graceful
DISCONNECT
. - Key LWT parameters include the Will Topic, Will Message, Will QoS, and Will Retain flag.
- Setting Will Retain to
true
is common for status messages, allowing new subscribers to get the last known state. - If LWT is retained, the device should publish an “online” message (with retain) to the same topic upon reconnection to update the status.
- Configuration in ESP-IDF is done via the
.session.last_will
member of theesp_mqtt_client_config_t
structure. - LWT functionality and its configuration are consistent across all ESP32 variants using ESP-IDF.
Further Reading
- HiveMQ MQTT Essentials – Part 9: Last Will and Testament:
- EMQx Blog: MQTT Last Will and Testament (LWT) Use Cases and Examples:
- ESP-IDF Programming Guide – MQTT Client:
- https://docs.espressif.com/projects/esp-idf/en/v5.2.1/esp32/api-reference/protocols/mqtt.html (Check the section on
esp_mqtt_client_config_t
for LWT fields).
- https://docs.espressif.com/projects/esp-idf/en/v5.2.1/esp32/api-reference/protocols/mqtt.html (Check the section on
- OASIS MQTT Version 3.1.1 Specification:
- http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718030 (Section 3.1.2.5 Will Flag, Will QoS, Will Retain)
- OASIS MQTT Version 5.0 Specification: (If working with MQTT 5.0 brokers/clients, LWT has more advanced options like Will Delay Interval and Message Properties)
- https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901208 (Section 3.1.2.5 Will Flag)
