/*********************************************************** * author: LaoHuang * create: 2022-04-14 * email:rememberyousaid@163.com * source:https://github.com/kerwincui/wumei-smart * board:esp32 版本1.0.6 ***********************************************************/ #include "Helper.h" String g_time; WiFiClient wifiClient; PubSubClient mqttClient; float rssi = 0; char wumei_iv[17] = "wumei-smart-open"; int monitorCount = 0; long monitorInterval = 1000; //==================================== 这是需要配置的项 =============================== // Wifi配置 char *wifiSsid = "wifi-ssid"; char *wifiPwd = "wifi-password"; // 设备信息配置 String deviceNum = "D6329VL54419L1Y0"; String userId = "1"; String productId = "2"; float firmwareVersion = 1.0; // 经度和纬度可选,如果产品使用设备定位,则必须传 float latitude=0; float longitude=0; // Mqtt配置 char *mqttHost = "www.baidu.com"; int mqttPort = 1883; char *mqttUserName = "wumei-smart"; char *mqttPwd = "P5FJKZJHIR82GNB2"; char mqttSecret[17] = "K63C4EA3AI5TER97"; // 产品启用授权码,则授权码不能为空 String authCode=""; // NTP地址(用于获取时间,可选的修改为自己部署项目的地址) String ntpServer = "http://www.baidu.com:8080/iot/tool/ntp?deviceSendTime="; //==================================================================================== // 订阅的主题 String prefix = "/" + productId + "/" + deviceNum; String sOtaTopic = prefix + "/ota/get"; String sNtpTopic = prefix + "/ntp/get"; String sPropertyTopic = prefix + "/property/get"; String sFunctionTopic = prefix + "/function/get"; String sPropertyOnline = prefix + "/property-online/get"; String sFunctionOnline = prefix + "/function-online/get"; String sMonitorTopic = prefix + "/monitor/get"; // 发布的主题 String pInfoTopic = prefix + "/info/post"; String pNtpTopic = prefix + "/ntp/post"; String pPropertyTopic = prefix + "/property/post"; String pFunctionTopic = prefix + "/function/post"; String pMonitorTopic = prefix + "/monitor/post"; String pEventTopic = prefix + "/event/post"; // 物模型-属性处理 void processProperty(String payload) { StaticJsonDocument<1024> doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print(F("deserializeJson() failed: ")); Serial.println(error.f_str()); return; } for (JsonObject object : doc.as()) { // 匹配云端定义的属性(不包含属性中的监测数据) const char *id = object["id"]; const char *value = object["value"]; printMsg((String)id + ":" + (String)value); } // 最后发布属性,服务端订阅存储(重要) publishProperty(payload); } // 物模型-功能处理 void processFunction(String payload) { StaticJsonDocument<1024> doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print(F("deserializeJson() failed: ")); Serial.println(error.f_str()); return; } for (JsonObject object : doc.as()) { // 匹配云端定义的功能 const char *id = object["id"]; const char *value = object["value"]; if (strcmp(id, "switch") == 0) { printMsg("开关 switch:" + (String)value); } else if (strcmp(id, "gear") == 0) { printMsg("档位 gear:" + (String)value); } else if (strcmp(id, "light_color") == 0) { printMsg("灯光颜色 light_color:" + (String)value); } else if (strcmp(id, "message") == 0) { printMsg("屏显消息 message:" + (String)value); } else if (strcmp(id, "report_monitor") == 0) { String msg = randomPropertyData(); printMsg("订阅到上报监测数据指令,上报数据:"); printMsg(msg); publishProperty(msg); } } // 最后发布功能,服务端订阅存储(重要) publishFunction(payload); } // Mqtt回调 void callback(char *topic, byte *payload, unsigned int length) { blink(); printMsg("接收数据:"); String data = ""; for (int i = 0; i < length; i++) { Serial.print((char)payload[i]); data += (char)payload[i]; } if (strcmp(topic, sOtaTopic.c_str()) == 0) { printMsg("订阅到设备升级指令..."); StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print(F("deserializeJson() failed: ")); Serial.println(error.f_str()); return; } String newVersion = doc["version"]; String downloadUrl = doc["downloadUrl"]; printMsg("固件版本:"+newVersion); printMsg("下载地址:"+downloadUrl); } else if (strcmp(topic, sNtpTopic.c_str()) == 0) { printMsg("订阅到NTP时间..."); StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print(F("deserializeJson() failed: ")); Serial.println(error.f_str()); return; } float deviceSendTime = doc["deviceSendTime"]; float serverSendTime = doc["serverSendTime"]; float serverRecvTime = doc["serverRecvTime"]; float deviceRecvTime = millis(); float now = (serverSendTime + serverRecvTime + deviceRecvTime - deviceSendTime) / 2; printMsg("当前时间:" + String(now, 0)); } else if (strcmp(topic, sPropertyTopic.c_str()) == 0 || strcmp(topic, sPropertyOnline.c_str()) == 0) { printMsg("订阅到属性指令..."); processProperty(data); } else if (strcmp(topic, sFunctionTopic.c_str()) == 0 || strcmp(topic, sFunctionOnline.c_str()) == 0) { printMsg("订阅到功能指令..."); processFunction(data); } else if (strcmp(topic, sMonitorTopic.c_str()) == 0) { printMsg("订阅到实时监测指令..."); StaticJsonDocument<128> doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print(F("deserializeJson() failed: ")); Serial.println(error.f_str()); return; } monitorCount = doc["count"]; monitorInterval = doc["interval"]; } } // 连接wifi void connectWifi() { printMsg("连接 "); Serial.print(wifiSsid); WiFi.mode(WIFI_STA); WiFi.begin(wifiSsid, wifiPwd); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } printMsg("WiFi连接成功"); printMsg("IP地址: "); Serial.print(WiFi.localIP()); } // 连接mqtt void connectMqtt() { printMsg("连接Mqtt服务器..."); // 生成mqtt认证密码(设备加密认证,密码加密格式为:mqtt密码 & 过期时间 & 授权码,其中授权码为可选) String password = generationPwd(); String encryptPassword = encrypt(password, mqttSecret, wumei_iv); printMsg("密码(已加密):" + encryptPassword); mqttClient.setClient(wifiClient); mqttClient.setServer(mqttHost, mqttPort); mqttClient.setCallback(callback); mqttClient.setBufferSize(1024); mqttClient.setKeepAlive(10); //连接 设备mqtt客户端Id格式为:认证类型(E=加密、S=简单) & 设备编号 & 产品ID & 用户ID String clientId = "E&" + deviceNum + "&" + productId +"&" + userId; bool connectResult = mqttClient.connect(clientId.c_str(), mqttUserName, encryptPassword.c_str()); if (connectResult) { printMsg("连接成功"); // 订阅(OTA、NTP、属性、功能、实时监测) mqttClient.subscribe(sOtaTopic.c_str(), 1); mqttClient.subscribe(sNtpTopic.c_str(), 1); mqttClient.subscribe(sPropertyTopic.c_str(), 1); mqttClient.subscribe(sFunctionTopic.c_str(), 1); mqttClient.subscribe(sPropertyOnline.c_str(), 1); mqttClient.subscribe(sFunctionOnline.c_str(), 1); mqttClient.subscribe(sMonitorTopic.c_str(), 1); printMsg("订阅主题:" + sOtaTopic); printMsg("订阅主题:" + sNtpTopic); printMsg("订阅主题:" + sPropertyTopic); printMsg("订阅主题:" + sFunctionTopic); printMsg("订阅主题:" + sPropertyOnline); printMsg("订阅主题:" + sFunctionOnline); printMsg("订阅主题:" + sMonitorTopic); // 发布设备信息 publishInfo(); } else { printMsg("连接失败, rc="); Serial.print(mqttClient.state()); } } // 1.发布设备信息 void publishInfo() { StaticJsonDocument<256> doc; doc["rssi"] = WiFi.RSSI(); doc["firmwareVersion"] = firmwareVersion; doc["status"] = 3; // (1-未激活,2-禁用,3-在线,4-离线) doc["userId"] = (String)userId; doc["longitude"] = longitude; //经度 可选 doc["latitude"] = latitude; // 纬度 可选 // 设备摘要,可选(自定义配置信息,不限数量) JsonObject summary = doc.createNestedObject("summary"); summary["name"]="wumei-smart"; summary["chip"]="esp8266"; summary["author"]="kerwincui"; summary["version"]=1.6; summary["create"]="2022-06-06"; printMsg("发布设备信息:"); serializeJson(doc, Serial); String output; serializeJson(doc, output); mqttClient.publish(pInfoTopic.c_str(), output.c_str()); } // 2.发布时钟同步信,用于获取当前时间(可选) void publishNtp() { StaticJsonDocument<128> doc; doc["deviceSendTime"] = millis(); printMsg("发布NTP信息:"); serializeJson(doc, Serial); String output; serializeJson(doc, output); mqttClient.publish(pNtpTopic.c_str(), output.c_str()); } // 3.发布属性 void publishProperty(String msg) { printMsg("发布属性:" + msg); mqttClient.publish(pPropertyTopic.c_str(), msg.c_str()); } // 4.发布功能 void publishFunction(String msg) { printMsg("发布功能:" + msg); mqttClient.publish(pFunctionTopic.c_str(), msg.c_str()); } // 5.发布事件 void publishEvent() { // 匹配云端的事件 StaticJsonDocument<512> doc; JsonObject objTmeperature = doc.createNestedObject(); objTmeperature["id"] = "height_temperature"; objTmeperature["value"] = "40"; objTmeperature["remark"] = "温度过高警告"; JsonObject objException = doc.createNestedObject(); objException["id"] = "exception"; objException["value"] = "异常消息,消息内容XXXXXXXX"; objException["remark"] = "设备发生错误"; printMsg("发布事件:"); serializeJson(doc, Serial); String output; serializeJson(doc, output); mqttClient.publish(pEventTopic.c_str(), output.c_str()); } // 6.发布实时监测数据 void publishMonitor() { String msg = randomPropertyData(); // 发布为实时监测数据,不会存储 printMsg("发布实时监测数据:" + msg); mqttClient.publish(pMonitorTopic.c_str(), msg.c_str()); } // 随机生成监测值 String randomPropertyData() { // 匹配云端定义的监测数据,随机数代替监测结果 float randFloat = 0; int randInt = 0; StaticJsonDocument<1024> doc; JsonObject objTmeperature = doc.createNestedObject(); objTmeperature["id"] = "temperature"; randFloat = random(1000, 3000); objTmeperature["value"] = (String)(randFloat / 100); objTmeperature["remark"] = (String)millis(); JsonObject objHumidity = doc.createNestedObject(); objHumidity["id"] = "humidity"; randFloat = random(3000, 6000); objHumidity["value"] = (String)(randFloat / 100); objHumidity["remark"] = (String)millis(); JsonObject objCo2 = doc.createNestedObject(); objCo2["id"] = "co2"; randInt = random(400, 1000); objCo2["value"] = (String)(randInt); objCo2["remark"] = (String)millis(); JsonObject objBrightness = doc.createNestedObject(); objBrightness["id"] = "brightness"; randInt = random(1000, 10000); objBrightness["value"] = (String)(randInt); objBrightness["remark"] = (String)millis(); printMsg("随机生成监测数据值:"); serializeJson(doc, Serial); String output; serializeJson(doc, output); return output; } // 生成密码 String generationPwd() { String jsonTime = getTime(); printMsg("getTime()= " + jsonTime); // 128字节内存池容量 StaticJsonDocument<128> doc; // 解析JSON DeserializationError error = deserializeJson(doc, jsonTime); if (error) { printMsg("Json解析失败:"); Serial.print(error.f_str()); return ""; } // 获取当前时间 float deviceSendTime = doc["deviceSendTime"]; float serverSendTime = doc["serverSendTime"]; float serverRecvTime = doc["serverRecvTime"]; float deviceRecvTime = millis(); float now = (serverSendTime + serverRecvTime + deviceRecvTime - deviceSendTime) / 2; // 过期时间 = 当前时间 + 1小时 float expireTime = now + 1 * 60 * 60 * 1000; // 密码加密格式为:mqtt密码 & 过期时间 & 授权码(可选),如果产品启用了授权码就必须加上 String password=""; if(authCode == ""){ password = (String)mqttPwd + "&" + String(expireTime, 0); }else{ password = (String)mqttPwd + "&" + String(expireTime, 0) + "&" + authCode; } printMsg("密码(未加密):" + password); return password; } // HTTP获取时间 String getTime() { while (WiFi.status() == WL_CONNECTED) { HTTPClient http; printMsg("获取时间..."); if (http.begin(wifiClient, (ntpServer + (String)millis()).c_str())) { // 发送请求 int httpCode = http.GET(); if (httpCode > 0) { if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { g_time = http.getString(); printMsg("获取时间成功,data:"); Serial.print(g_time); return g_time; } } else { printMsg("获取时间失败,error:"); Serial.printf(http.errorToString(httpCode).c_str()); } http.end(); } else { printMsg("连接Http失败"); } delay(500); } } //打印提示信息 void printMsg(String msg) { Serial.print("\r\n["); Serial.print(millis()); Serial.print("ms]"); Serial.print(msg); } // 控制指示灯闪烁 void blink() { printMsg("指示灯闪烁..."); pinMode(15, OUTPUT); for (int i = 0; i < 2; i++) { digitalWrite(15, HIGH); delay(200); digitalWrite(15, LOW); delay(200); } } // 加密 (AES-CBC-128-pkcs5padding) String encrypt(String plain_data, char *wumei_key, char *wumei_iv) { int i; // pkcs7padding填充 Block Size : 16 int len = plain_data.length(); int n_blocks = len / 16 + 1; uint8_t n_padding = n_blocks * 16 - len; uint8_t data[n_blocks * 16]; memcpy(data, plain_data.c_str(), len); for (i = len; i < n_blocks * 16; i++) { data[i] = n_padding; } uint8_t key[16], iv[16]; uint8_t crypt_data[3 * 16] = {0}; memcpy(key, wumei_key, 16); memcpy(iv, wumei_iv, 16); memset(crypt_data, 0, 48); len = n_blocks * 16; // 加密 mbedtls_aes_context aes_ctx; mbedtls_aes_init(&aes_ctx); mbedtls_aes_setkey_enc(&aes_ctx, key, 128); mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_ENCRYPT, len, iv, data, crypt_data); // Base64编码 char encoded_data[base64_enc_len(len)]; base64_encode(encoded_data, (char *)crypt_data, len); return String(encoded_data); } // 解密 (AES-CBC-128-pkcs5padding) String decrypt(String encoded_data_str, char *wumei_key, char *wumei_iv) { int input_len = encoded_data_str.length(); char *encoded_data = const_cast(encoded_data_str.c_str()); int len = base64_dec_len(encoded_data, input_len); uint8_t data[len]; base64_decode((char *)data, encoded_data, input_len); uint8_t key[16], iv[16]; memcpy(key, wumei_key, 16); memcpy(iv, wumei_iv, 16); int n_blocks = len / 16; uint8_t n_padding = data[n_blocks * 16 - 1]; len = n_blocks * 16 - n_padding; char plain_data[len + 1]; //密文空间 mbedtls_aes_context aes_ctx; mbedtls_aes_init(&aes_ctx); //设置解密密钥 mbedtls_aes_setkey_dec(&aes_ctx, key, 128); mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_DECRYPT, 48, iv, (unsigned char *)encoded_data, (unsigned char *)plain_data); mbedtls_aes_free(&aes_ctx); // PKCS#7 Padding 填充 plain_data[len] = '\0'; return String(plain_data); }