0 引言
冬天床边没有开关,睡觉懒得关灯,想通过小爱同学控制灯的开关,但是不想换开关。
所以 想用ESP32接入米家,控制一个舵机实现开关控制。
文章目录
- 0 引言
- 1 MQTT协议
- 2 ESP32 MQTT例程
- 2.1 ESP-MQTT 库
- 2.2.1 配置结构体 esp_mqtt_client_config_t
- 2.2.2 事件
- 2.2 例程调试
- 2.3 例程分析
- 3 连接巴法平台
- 3.1 配置巴法平台
- 3.2 修改例程代码
- 3.3 设置小爱同学
1 MQTT协议
Message Queuing Telemetry Transport,消息队列传输探测
ISO 标准下的一种基于发布-订阅模式的消息协议,基于 TCP/IP 协议簇,用于 IoT 即物联网上。
相对于HTTP的优点:
- 数据包开销小,易于传输。
- MQTT客户端容易实现。
发布-订阅模式pub/sub
传统的 客户端-服务器架构:服务器与客户端直接通信。
发布-订阅模式:发布者publisher 与 订阅者 subscribers 分离,不会直接通信,由第三方组件broker代理。
publisher 与 subscriber 在 空间、时间和同步 三个维度解耦。
MQTT协议数据包结构
巴法支持支持 MQTT3.1.1 协议,支持Qos0 Qos1,支持retian保留消息,所以我也只看了一下MQTT3.1.1 协议
MQTT3.1.1 协议文档
- Fixed header, present in all MQTT Control Packets
- Variable header, present in some MQTT Control Packets
- Payload, present in some MQTT Control Packets
broker过滤消息的选项
broker 使得 subscriber 只接收自己需要的消息。broker 可以基于以下选项过滤消息:
- 基于主题
- 基于内容
- 基于类型
服务质量级别
- QoS 0:最多传送一次
反正 receiver 有没有响应我都之发送一次。 - QoS 1:要实施至少一次传送
我要确保 receiver 至少收到了一次消息。 - QoS 2:正好一次传送
我要确保消息不丢失且不重复。
有了这些基础知识,就可以基于ESP32 的官方例程来实现我们想要的功能了。
2 ESP32 MQTT例程
2.1 ESP-MQTT 库
2.2.1 配置结构体 esp_mqtt_client_config_t
通过 esp_mqtt_client_config_t 结构体配置,配置结构体有以下子结构来配置客户端操作的不同方面。
- broker : 设置地址和安全验证。
可以通过 uri 字段或 hostname 、transport 和 port 的组合来配置。
uri组成:scheme://hostname:port/path,MQTT TCP例程uri为mqtt://mqtt.eclipseprojects.io,默认端口1883 - credentials : 用于身份验证的客户端凭证。
username:指向连接到broker的username的指针,也可以通过URI设置。
client_id:指向客户端id的指针,默认为ESP32_%CHIPID%,其中%CHIPID%是MAC地址的最后3个字节,采用十六进制格式 - session : MQTT会话方面的配置。
topic: 指向 LWT(Last Will and Testament) message topic,LWT就是客户死(断开)前用来通知别人的信息。
msg: 指向 LWT message
msg_len: LWT message 长度
qos: 指向 LWT message qos
retain: LWT message 保留标志 - network : 组网相关配置。
- task : 配置FreeRTOS任务。
- buffer : 输入和输出的缓冲区大小。
esp_mqtt_client_config_t 结构:
/**
* MQTT client configuration structure
*/
typedef struct {
mqtt_event_callback_t event_handle; /*!< handle for MQTT events as a callback in legacy mode */
esp_event_loop_handle_t event_loop_handle; /*!< handle for MQTT event loop library */
const char *host; /*!< MQTT server domain (ipv4 as string) */
const char *uri; /*!< Complete MQTT broker URI */
uint32_t port; /*!< MQTT server port */
const char *client_id; /*!< default client id is ``ESP32_%CHIPID%`` where %CHIPID% are last 3 bytes of MAC address in hex format */
const char *username; /*!< MQTT username */
const char *password; /*!< MQTT password */
const char *lwt_topic; /*!< LWT (Last Will and Testament) message topic (NULL by default) */
const char *lwt_msg; /*!< LWT message (NULL by default) */
int lwt_qos; /*!< LWT message qos */
int lwt_retain; /*!< LWT retained message flag */
int lwt_msg_len; /*!< LWT message length */
int disable_clean_session; /*!< mqtt clean session, default clean_session is true */
int keepalive; /*!< mqtt keepalive, default is 120 seconds */
bool disable_auto_reconnect; /*!< this mqtt client will reconnect to server (when errors/disconnect). Set disable_auto_reconnect=true to disable */
void *user_context; /*!< pass user context to this option, then can receive that context in ``event->user_context`` */
int task_prio; /*!< MQTT task priority, default is 5, can be changed in ``make menuconfig`` */
int task_stack; /*!< MQTT task stack size, default is 6144 bytes, can be changed in ``make menuconfig`` */
int buffer_size; /*!< size of MQTT send/receive buffer, default is 1024 (only receive buffer size if ``out_buffer_size`` defined) */
const char *cert_pem; /*!< Pointer to certificate data in PEM or DER format for server verify (with SSL), default is NULL, not required to verify the server. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in cert_len. */
size_t cert_len; /*!< Length of the buffer pointed to by cert_pem. May be 0 for null-terminated pem */
const char *client_cert_pem; /*!< Pointer to certificate data in PEM or DER format for SSL mutual authentication, default is NULL, not required if mutual authentication is not needed. If it is not NULL, also `client_key_pem` has to be provided. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in client_cert_len. */
size_t client_cert_len; /*!< Length of the buffer pointed to by client_cert_pem. May be 0 for null-terminated pem */
const char *client_key_pem; /*!< Pointer to private key data in PEM or DER format for SSL mutual authentication, default is NULL, not required if mutual authentication is not needed. If it is not NULL, also `client_cert_pem` has to be provided. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in client_key_len */
size_t client_key_len; /*!< Length of the buffer pointed to by client_key_pem. May be 0 for null-terminated pem */
esp_mqtt_transport_t transport; /*!< overrides URI transport */
int refresh_connection_after_ms; /*!< Refresh connection after this value (in milliseconds) */
const struct psk_key_hint *psk_hint_key; /*!< Pointer to PSK struct defined in esp_tls.h to enable PSK authentication (as alternative to certificate verification). If not NULL and server/client certificates are NULL, PSK is enabled */
bool use_global_ca_store; /*!< Use a global ca_store for all the connections in which this bool is set. */
esp_err_t (*crt_bundle_attach)(void *conf); /*!< Pointer to ESP x509 Certificate Bundle attach function for the usage of certification bundles in mqtts */
int reconnect_timeout_ms; /*!< Reconnect to the broker after this value in miliseconds if auto reconnect is not disabled (defaults to 10s) */
const char **alpn_protos; /*!< NULL-terminated list of supported application protocols to be used for ALPN */
const char *clientkey_password; /*!< Client key decryption password string */
int clientkey_password_len; /*!< String length of the password pointed to by clientkey_password */
esp_mqtt_protocol_ver_t protocol_ver; /*!< MQTT protocol version used for connection, defaults to value from menuconfig*/
int out_buffer_size; /*!< size of MQTT output buffer. If not defined, both output and input buffers have the same size defined as ``buffer_size`` */
bool skip_cert_common_name_check; /*!< Skip any validation of server certificate CN field, this reduces the security of TLS and makes the mqtt client susceptible to MITM attacks */
bool use_secure_element; /*!< enable secure element for enabling SSL connection */
void *ds_data; /*!< carrier of handle for digital signature parameters */
int network_timeout_ms; /*!< Abort network operation if it is not completed after this value, in milliseconds (defaults to 10s) */
bool disable_keepalive; /*!< Set disable_keepalive=true to turn off keep-alive mechanism, false by default (keepalive is active by default). Note: setting the config value `keepalive` to `0` doesn't disable keepalive feature, but uses a default keepalive period */
const char *path; /*!< Path in the URI*/
int message_retransmit_timeout; /*!< timeout for retansmit of failded packet */
} esp_mqtt_client_config_t;
2.2.2 事件
MQTT客户端可以发布以下事件:
MQTT_EVENT_BEFORE_CONNECT:客户端已初始化,即将开始连接到代理。
MQTT_EVENT_CONNECTED:客户端已成功建立到代理的连接。客户机现在已经准备好发送和接收数据了。
MQTT_EVENT_DISCONNECTED:客户端由于无法读取或写入数据而终止了连接,例如由于服务器不可用。
MQTT_EVENT_SUBSCRIBED:代理已经确认客户机的订阅请求。事件数据将包含订阅消息的消息ID。
MQTT_EVENT_UNSUBSCRIBED:代理已经确认客户端的取消订阅请求。事件数据将包含取消订阅消息的消息ID。
MQTT_EVENT_PUBLISHED:代理已经确认客户机的发布消息。这将仅针对服务质量级别1和2发布,因为级别0不使用确认。事件数据将包含发布消息的消息ID。
MQTT_EVENT_DATA:客户端已收到发布消息。事件数据包含:消息ID、消息发布到的主题的名称、接收到的数据及其长度。对于超过内部缓冲区的数据,将发布多个MQTT_EVENT_DATA,并更新来自事件数据的current_data_offset和total_data_len,以跟踪碎片消息。
MQTT_EVENT_ERROR:客户端遇到错误。事件数据中的Esp_mqtt_error_type_t from error_handle可用于进一步确定错误的类型。错误的类型将决定error_handle结构体的哪些部分被填充。
2.2 例程调试
例程路径:[安装路径]\Espressif\frameworks\esp-idf-v4.4\examples\protocols\mqtt\tcp
在vscode 使用 menuconfig 配置WIFI名称和密码
调试打印输出:
可以看到,例程接入测试接口然后完成了一些消息的传输。
2.3 例程分析
-
首先通过 uri 字段 配置 broker 。URI字段为 mqtt://mqtt.eclipseprojects.io,主题mqtt,主机名 mqtt.eclipseprojects.io,默认端口1883
-
esp_mqtt_client_init(&mqtt_cfg);
根据配置创建MQTT客户端句柄。 -
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
注册MQTT事件。回调函数为 mqtt_event_handler -
esp_mqtt_client_start(client);
使用已经创建的客户句柄启动MQTT客户端。
下面分析回调函数 mqtt_event_handler 中 MQTT MQTT_EVENT_CONNECTED 事件处理:
MQTT_EVENT_CONNECTED:客户端已成功建立到代理的连接。客户机现在已经准备好发送和接收数据了。
esp_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0);
客户端向代理发送发布消息。client为例子初始化的client,topic为"/topic/qos1",数据为"data_3",长度设为0表示自己计算,1表示服务质量为Qos 1,LWT message 不保留。
sp_mqtt_client_subscribe(client, "/topic/qos0", 0);
esp_mqtt_client_subscribe(client, "/topic/qos1", 1);
esp_mqtt_client_unsubscribe(client, "/topic/qos1");
从已定义的主题取消订阅客户端。
可以看到,我们调试的输出与代码是吻合的:
I (9116) MQTT_EXAMPLE: sent publish successful, msg_id=14555
I (9116) MQTT_EXAMPLE: sent subscribe successful, msg_id=37860
I (9116) MQTT_EXAMPLE: sent subscribe successful, msg_id=49262
I (9126) MQTT_EXAMPLE: sent unsubscribe successful, msg_id=28076
3 连接巴法平台
3.1 配置巴法平台
巴法平台作为 broker ,配置起来非常方便,官方文档。
准备工作,注册巴法平台:
- 在官网注册一个账号。
- 新建一个MQTT设备云主题。具体可以查阅官方文档的 3、平台使用教程 章节。这里注意创建的是MQTT设备云而不是TCP设备云就行。新建好之后点击昵称可以修改昵称,我这里修改为“卧室灯”。
设备命名是有讲究的,这里引用官方文档的内容:
11.1 接入介绍 巴法云物联网平台默认接入米家,仅支持以下类型的设备:插座、灯泡、风扇、传感器、空调、开关、窗帘。 用户可以自主选择是否接入米家,根据主题名字判定。 当主题名字后三位是001时为插座设备。 当主题名字后三位是002时为灯泡设备。
当主题名字后三位是003时为风扇设备。 当主题名字后三位是004时为传感器设备。 当主题名字后三位是005时为空调设备。
当主题名字后三位是006时为开关设备。 当主题名字后三位是009时为窗帘设备。
- 保存好 私钥,主题名称。
3.2 修改例程代码
修改例程代码:
app_main.c 加入ID_MQTT
const char* bemfa_uri = "mqtt://bemfa.com:9501/"; // uri, scheme://hostname:port/path
const char* ID_MQTT = "7fe8xxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // 私钥作为MQTT 的ID
const char* topic = "light002" ; // 对应的topic。
mqtt_app_start()函数中配置uri和ID,代码修改为:
static void mqtt_app_start(void)
{
esp_mqtt_client_config_t mqtt_cfg = {
.uri = bemfa_uri,
.client_id = ID_MQTT
};
#if CONFIG_BROKER_URL_FROM_STDIN
char line[128];
if (strcmp(mqtt_cfg.uri, "FROM_STDIN") == 0) {
int count = 0;
printf("Please enter url of mqtt broker\n");
while (count < 128) {
int c = fgetc(stdin);
if (c == '\n') {
line[count] = '\0';
break;
} else if (c > 0 && c < 127) {
line[count] = c;
++count;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
mqtt_cfg.uri = line;
printf("Broker url: %s\n", line);
} else {
ESP_LOGE(TAG, "Configuration mismatch: wrong broker url");
abort();
}
#endif /* CONFIG_BROKER_URL_FROM_STDIN */
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);
}
MQTT事件回调函数mqtt_event_handler() 修改订阅事件,修改订阅的topic为巴法中设置的light002,qos设为0或1都可以的;另外就不需要往broker发送测试数据了,全部注释掉:
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=%d", 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");
// msg_id = esp_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0);
// ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
// msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0);
// ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
// msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1);
// ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
// msg_id = esp_mqtt_client_unsubscribe(client, "/topic/qos1");
// ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(client, topic, 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
// msg_id = esp_mqtt_client_publish(client, topic, "off", 0, 1, 0);
// ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
// msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0);
// ESP_LOGI(TAG, "sent publish successful, msg_id=%d", 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);
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->event_id);
break;
}
}
3.3 设置小爱同学
-
米家添加设备,我的 -> 其他平台设备 -> 找到巴法,然后绑定
-
小爱同学添加训练
-
使用小爱同学控制
这里我先后给了“打开卧室灯”与“关闭卧室灯”的指令。
可以看到,ESP32已经收到控制指令啦,至于怎么响应指令,就可以随意开发了,我后续打算添加一个舵机控制,来实现传统开关的通断控制。
参考:
esp8266接入小爱同学,通过mqtt
ESP8266、ESP32实现小爱语音控制灯