IoT (Internet of Things) 嘅概念係將硬件連接 internet,最大問題係各品牌嘅家用產品無法互相溝通,集中一個界面操控。DIY IoT 嘅最佳選擇係良率比較高用 ESP 8266 MCU 有 wifi 功能嘅 WeMos D1 R2 或者 D1 Mini Pro,最實用係用 I2C 連接温濕度 sensor SHT31,可以比較室內外温濕度差,作為調較冷氣嘅參考。
以香港潮濕嘅氣候並非對應日本或者歐洲冷氣机嘅設計,一定要將風速較到最低加强抽濕效果到 70% 以下先有舒適嘅感覺,而冷氣机一般都只有恒温控制而缺乏恒濕感應。單位入風口嘅温度可以同香港天文台嘅分區氣温差好遠,我個單位夏天一般高 3 度,而冬天更加高 6 度;我用紅外線測温儀測過,同大廈外牆温度吻合,所以成因係陽光直射加熱產生 heat capacitance。
一般 I2C sensor 都可以揾倒 Arduino library 直接抄 example 就用得,但我喺 SHT31嘅 library 裏面加入 酷熱指數 (heat index) 將温濕度数值合併提升參考價值,同埋刪除 delay 嘅寫法。ESP8266 有足夠運算能力加入 Task library 做到 multitasking 避免運用 delay。Arduino 獨特嘅 loop 係 1ms period,我用 Task library 喺 common library file 加多 20ms, 1000ms, 8640ms,對應 servo, web, wifi reconnect,一共四個運行速度就足夠取代運用 delay 減速。
#define BLYNK_PRINT Serial #define ARDUINOJSON_USE_DOUBLE 1 //******************************************************************************************* // include library //******************************************************************************************* #include <Adafruit_INA219.h> #include <ArduinoJson.h> #include <ArduinoOTA.h> #include <BlynkSimpleEsp8266.h> #include <ESP8266HTTPClient.h> #include <ESP8266NetBIOS.h> #include <ESP8266mDNS.h> #include <ESP8266WebServer.h> #include <ESP8266WiFi.h> #include <Task.h> #include <TimeLib.h> #include <WiFiUdp.h> #include <fn_sht31.h> //******************************************************************************************* // Function Prototypes //******************************************************************************************* char* cast_s2c(String string_input); double cast_s2d(String string_input); double round(double value, double dp); String pad2digit(uint8_t number); void fn_wemos_blynk_begin(const char mdns[], const char blynk_token[]); void fn_handle_int(); void fn_handle_encoder(bool encoder_cw); void fn_encoder_pin1_change(); void fn_encoder_pin2_change(); void fn_but_change(); void fn_counter(); void fn_ota_begin(const char mdns[], const uint8_t port); void fn_i2c_begin(uint8_t pin_sda, uint8_t pin_scl); void fn_sht_display(); void fn_ina_display(); void fn_wifi_begin(); void fn_ntp_sync(int8_t timeZone); void fn_handle_root(); void fn_handle_json(); void fn_handle_favicon(); void fn_handle_notfound(); void fn_cors_header(); //******************************************************************************************* // global & class objects //******************************************************************************************* const char ssid[] = "********"; const char password[] = "********"; const int8_t TIMEZONE = 8; bool enable_encoder = 0; bool enable_buildinled = 0; bool enable_counter = 0; bool enable_blynk = 0; bool enable_ap = 0; bool enable_root = 0; String string_json = ""; String string_i2c = ""; struct tm time_tm; time_t time_ntp = 0; StaticJsonDocument<512> json; HTTPClient http; ESP8266WebServer server(80); WiFiClient client; WiFiUDP udp; //******************************************************************************************* // hardware //******************************************************************************************* const uint8_t pin_rx = 3; //ws2812 LED const uint8_t pin_tx = 1; const uint8_t pin_a0 = 17; const uint8_t pin_d0 = 16; const uint8_t pin_d1 = 5; const uint8_t pin_d2 = 4; const uint8_t pin_d3 = 0; const uint8_t pin_d4 = 2; const uint8_t pin_d5 = 14; const uint8_t pin_d6 = 12; const uint8_t pin_d7 = 13; const uint8_t pin_d8 = 15; const uint8_t pin_standby = pin_d0; const uint8_t pin_scl = pin_d1; const uint8_t pin_sda = pin_d2; const uint8_t pin_int = pin_d3; const uint8_t pin_led = pin_d4; // LED_BUILTIN const uint8_t pin_pwm = pin_d5; const uint8_t pin_in1 = pin_d6; const uint8_t pin_in2 = pin_d7; const uint8_t pin_counter = pin_d8; const uint8_t sht_add = 68; const uint8_t ina_add = 64; bool array_i2c[127]; Adafruit_INA219 ina; SHT31 sht = SHT31(); //******************************************************************************************* // multi task loop //******************************************************************************************* TaskManager taskManager; void loop_20ms(uint32_t deltaTime); void loop_1000ms(uint32_t deltaTime); void loop_8640ms(uint32_t deltaTime); FunctionTask timer20(loop_20ms, MsToTaskTime(20)); FunctionTask timer1000(loop_1000ms, MsToTaskTime(1000)); FunctionTask timer8640(loop_8640ms, MsToTaskTime(8640)); void fn_loop_1ms(); void fn_loop_20ms(); void fn_loop_1000ms(); void fn_loop_8640ms(); void loop() { fn_loop_1ms(); taskManager.Loop(); } void loop_20ms(uint32_t deltaTime) { server.handleClient(); if (enable_blynk)Blynk.run(); json["uptime"] = round(millis() / 86400000.0, 6); string_json = ""; serializeJson(json, string_json); MDNS.update(); fn_loop_20ms(); } void loop_1000ms(uint32_t deltaTime) { json["heap"] = ESP.getFreeHeap(); // system_get_free_heap_size if (WiFi.status() == 3)json["now"] = now(); if (array_i2c[64])fn_ina_display(); if (array_i2c[68])fn_sht_display(); ArduinoOTA.handle(); fn_loop_1000ms(); } void loop_8640ms(uint32_t deltaTime) { fn_wifi_begin(); fn_loop_8640ms(); } //******************************************************************************************* // init wemos setup //******************************************************************************************* void fn_wemos_blynk_begin(const char mdns[], const char blynk_token[]) { // hardware json["dns"] = String(mdns); Serial.begin(115200); Serial.println(""); analogWriteFreq(38000); // IRremote analogWriteRange(255); pinMode(pin_d0, OUTPUT); digitalWrite(pin_d0, 0); // standby pinMode(pin_d4, OUTPUT); digitalWrite(pin_d4, 1); // led off pinMode(pin_d5, OUTPUT); analogWrite(pin_d5, 0); // pwm 0 if (enable_encoder)json["pwm"] = 0; pinMode(pin_a0, INPUT); if (enable_counter) { pinMode(pin_d8, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(pin_d8), fn_counter, FALLING); } else { pinMode(pin_d8, OUTPUT); analogWrite(pin_d8, 0); // pwm 0 } pinMode(pin_d3, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(pin_d3), fn_but_change, CHANGE);//CHANGE, FALLING, RISING, LOW if (enable_encoder) { pinMode(pin_d6, INPUT_PULLUP); pinMode(pin_d7, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(pin_d6), fn_encoder_pin1_change, CHANGE); attachInterrupt(digitalPinToInterrupt(pin_d7), fn_encoder_pin2_change, CHANGE); } else { pinMode(pin_d6, INPUT); pinMode(pin_d7, INPUT); } // network WiFi.disconnect(); WiFi.hostname(mdns); WiFi.mode(WIFI_STA); http.setReuse(true); if (!enable_root)server.on("/", fn_handle_root); server.on("/json", fn_handle_json); server.on("/favicon.ico", fn_handle_favicon); server.onNotFound(fn_handle_notfound); server.on("/restart", []() { fn_cors_header(); server.send(204); ESP.restart(); }); server.on("/standby", []() { fn_cors_header(); server.send(204); digitalWrite(pin_standby, 0); analogWrite(pin_pwm, 0); json["pwm"] = 0; }); server.begin(); MDNS.begin(mdns); MDNS.addService("tcp", "tcp", 80); MDNS.addService("http", "tcp", 80); fn_ota_begin(mdns, 8266); udp.begin(80);; if (enable_blynk)Blynk.begin(blynk_token, ssid, password); fn_i2c_begin(pin_sda, pin_scl); fn_ntp_sync(TIMEZONE); taskManager.StartTask(&timer20); taskManager.StartTask(&timer1000); taskManager.StartTask(&timer8640); } //******************************************************************************************* // functions //******************************************************************************************* void fn_wifi_begin() { static uint8_t wifi_reconnect = 0; if (WiFi.getMode() == 1) { // 1=station, 2=ap //Serial.println("STATION mode"); if (WiFi.status() != 3) { // 0=idle, 3=connected, 4=fail, 7=disconnect WiFi.disconnect(); wifi_reconnect++; WiFi.begin(ssid, password); } else { if (year() < 2000) { fn_ntp_sync(TIMEZONE); } json["ip"] = WiFi.localIP().toString(); json["rssi"] = WiFi.RSSI(); wifi_reconnect = 0; } } else if (enable_ap && wifi_reconnect >= 2) { Serial.println("AP fallback"); WiFi.disconnect(); WiFi.mode(WIFI_AP); WiFi.softAP(String(json["dns"]), password); wifi_reconnect = 0; } else if (WiFi.getMode() == 2) { //Serial.println("AP mode"); json["ip"] = WiFi.softAPIP().toString(); wifi_reconnect = 0; } } void fn_ota_begin(const char mdns[], const uint8_t port) { ArduinoOTA.setPort(port); ArduinoOTA.setHostname(mdns); ArduinoOTA.onStart([]() { String type; if (ArduinoOTA.getCommand() == U_FLASH) { type = "sketch"; } else { // U_FS type = "filesystem"; } Serial.println("Start updating " + type); }); ArduinoOTA.begin(); } //******************************************************************************************* // cast //******************************************************************************************* char* cast_s2c(String string_input) { return (char*)string_input.c_str(); } double cast_s2d(String string_input) { return (double)string_input.toDouble(); } double round(double value, double dp) { double int_value = (int64_t)(value * pow(10, dp) + 0.5); return (double)(int_value / pow(10, dp)); } String pad2digit(uint8_t number) { if (number < 10) { String s = String(number); return '0' + s; } else { String s = String(number); return s; } } //******************************************************************************************* // I2C //******************************************************************************************* void fn_i2c_begin(uint8_t pin_sda, uint8_t pin_scl) { uint8_t error, address; uint8_t nDevices; Wire.begin(pin_sda, pin_scl); Wire.setClock(400000UL); nDevices = 0; string_i2c = "I2C: "; for (address = 1; address < 127; address++) { array_i2c[address] = false; Wire.beginTransmission(address); error = Wire.endTransmission(); if (error == 0) { if (address == sht_add) { sht.begin(sht_add); string_i2c += "sht"; array_i2c[address] = true; } else if (address == ina_add) { ina.begin(); string_i2c += "ina"; array_i2c[address] = true; } string_i2c += "("; string_i2c += address; string_i2c += ") "; nDevices++; } } if (nDevices == 0) { if (error != 4) { string_i2c += "no device"; } else { string_i2c += "error"; } } Serial.println(string_i2c); } void fn_ina_display() { int16_t shuntvoltage = 0; int16_t busvoltage = 0; int16_t current_mA = 0; int16_t loadvoltage = 0; double loadpower = 0.0; shuntvoltage = ina.getShuntVoltage_mV(); busvoltage = ina.getBusVoltage_V() * 1000; current_mA = ina.getCurrent_mA(); loadvoltage = busvoltage + shuntvoltage; loadpower = abs(current_mA * loadvoltage / 1000.0); if (loadpower == 0.0) { current_mA = 0; loadvoltage = 0; } json["current(mA)"] = current_mA; json["voltage(V)"] = round(loadvoltage / 1000.0, 2); json["power(mW)"] = round(loadpower, 0); } void fn_sht_display() { static bool sht_toggle = true; if (sht_toggle) { sht.request(); } else { double t = sht.readTemperature(); double h = sht.readHumidity(); double d = sht.readHeatIndex(); if (h > 0) { json["temperature(c)"] = round(t, 1); json["humidity(%)"] = round(h, 0); json["heatindex(c)"] = round(d, 1); } } sht_toggle = !sht_toggle; } //******************************************************************************************* // interupt //******************************************************************************************* volatile static bool pin1_press = false; // volatile for interupt volatile static bool pin2_press = false; // volatile for interupt ICACHE_RAM_ATTR void fn_encoder_pin1_change() { volatile static uint32_t encoder_pin1_time = -1; pin1_press = !digitalRead(pin_in1); if (pin1_press) { encoder_pin1_time = millis(); } else { volatile uint32_t pin1_holdtime = millis() - encoder_pin1_time; // Serial.println(pin1_holdtime); if (pin1_holdtime >= 0 && pin1_holdtime < 80) { // debounce pin1_press = true; if (!pin2_press) { // Serial.println("TF"); fn_handle_encoder(false); } } encoder_pin1_time = -1; pin1_press = false; } } ICACHE_RAM_ATTR void fn_encoder_pin2_change() { volatile static uint32_t encoder_pin2_time = -1; pin2_press = !digitalRead(pin_in2); if (pin2_press) { encoder_pin2_time = millis(); } else { volatile uint32_t pin2_holdtime = millis() - encoder_pin2_time; // Serial.println(pin2_holdtime); if (pin2_holdtime >= 0 && pin2_holdtime < 80) { // debounce pin2_press = true; if (!pin1_press) { // Serial.println("FT"); fn_handle_encoder(true); } } encoder_pin2_time = -1; pin2_press = false; } } ICACHE_RAM_ATTR void fn_but_change() { volatile static uint32_t but_presstime = 0; volatile static bool but_press = false; but_press = !digitalRead(pin_int); if (but_press) { but_presstime = millis(); } else { but_presstime = millis() - but_presstime; if (but_presstime < 600) { fn_handle_int(); } but_presstime = -1; } } ICACHE_RAM_ATTR void fn_counter() { volatile static uint32_t but_oldtime = 0; if (millis() - but_oldtime >= 4) { //debounce max rps = 125 json["rps"] = round(1000 / 2.0 / (double)(millis() - but_oldtime), 2); but_oldtime = millis(); } } //******************************************************************************************* // http //******************************************************************************************* void fn_ntp_sync(int8_t timeZone) { configTime(0, 0, "pool.ntp.org", "asia.pool.ntp.org"); uint32_t beginWait = millis(); while ((millis() - beginWait < 1000) && (time_ntp < 16 * 60 * 60)) { delay(200); time_ntp = time(nullptr); if (time_ntp > 16 * 60 * 60) { time_ntp = time_ntp + 1 + (timeZone * 60 * 60); setTime(time_ntp); gmtime_r(&time_ntp, &time_tm); Serial.print("time: "); Serial.println(pad2digit(hour()) + ":" + pad2digit(minute()) + ":" + pad2digit(second())); } } } void fn_handle_root() { fn_cors_header(); server.setContentLength(CONTENT_LENGTH_UNKNOWN); server.send(200, "text/html", ""); server.sendContent("<!DOCTYPE HTML><html><head>"); server.sendContent("<link rel='stylesheet' href='http://chanchunghoi.com/css/common.css' type='text/css'>"); server.sendContent("<link rel='stylesheet' href='http://chanchunghoi.com/wemos/esp8266wifi.css' type='text/css'>"); server.sendContent("<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js'></script>"); server.sendContent("<script src='http://chanchunghoi.com/wemos/esp8266wifi.js'></script>"); server.sendContent("</head><body><div id='json'>"); server.sendContent(string_json); server.sendContent("</div>"); server.sendContent("</body></html>"); } void fn_handle_json() { fn_cors_header(); server.setContentLength(string_json.length()); server.sendHeader("cache-control", "max-age=1"); server.send(200, "application/json", string_json); } void fn_handle_notfound() { String message = "File Not Foundnn"; message += "URI: "; message += server.uri(); message += "nMethod: "; message += (server.method() == HTTP_GET) ? "GET" : "POST"; message += "nArguments: "; message += server.args(); message += "n"; for (uint8_t i = 0; i < server.args(); i++) { message += " " + server.argName(i) + ": " + server.arg(i) + "n"; } server.send(404, "text/plain", message); } void fn_handle_favicon() { server.send(200, "image/x-icon", "<link rel="icon" href="data:;base64,=">"); } void fn_cors_header() { server.sendHeader("Access-Control-Allow-Methods", "PUT,GET,POST,OPTIONS"); server.sendHeader("Access-Control-Allow-Origin", "*"); }
軟件比硬件容易開發,例如 router 咁只需要用網頁作為介面而唔需要電子面版;喺 ESP8266 加入 web server 一樣做到同樣效果。ESP8266WebServer 比 Arduino web server 簡單同功能更强大,但絶對比唔上 Apache 之類嘅 web server。Arduino 係 C++ compiled language,比起 javascript 之類嘅 interpreted language 有高效能嘅優點,但比 javascript 難寫又需要 compile 嘥時間,所以最好盡量用 ESP8266 WebServer 經 JSON data format 輸出到 Apache,用一個網頁整合幾部 Wemos D1 嘅数据。
#define BLYNK_TEMPLATE_ID "TMPLLb--****" #define BLYNK_DEVICE_NAME "inlet" #define BLYNK_AUTH_TOKEN "*********************************" #include <fn_wemos_d1.h> const char mdns[] = "inlet"; //******************************************************************* // Instantiate class objects, global var and Function Prototypes //******************************************************************* uint8_t blynk_v5 = 0; // intake pwm fan speed control BLYNK_CONNECTED() { Blynk.syncAll(); } BLYNK_WRITE(V5) { blynk_v5 = param.asInt(); } //******************************************************************* // setup //******************************************************************* void setup() { ENABLE_BLYNK = 1; fn_wemos_blynk_begin(mdns, BLYNK_AUTH_TOKEN); analogWrite(pin_pwm, 191); json["pwm"] = 191; } //******************************************************************* // loop //******************************************************************* ICACHE_RAM_ATTR void fn_handle_int() {} ICACHE_RAM_ATTR void fn_handle_encoder(bool encoder_cw) {} void fn_loop_1ms() {} void fn_loop_20ms() {} void fn_loop_1000ms() { //auto adjust intake fan speed if (blynk_v5 > 0) { if (json["heatindex(c)"].as<double>() > 32 && json["heatindex-d(c)"].as<double>() < -2 && json["heatindex-d(c)"].as<double>() >= -4) { analogWrite(pin_pwm, constrain(blynk_v5 - 48, 77, 255)); json["pwm"] = constrain(blynk_v5 - 48, 77, 255); } else if (json["heatindex(c)"].as<double>() > 32 && json["heatindex-d(c)"].as<double>() < -4) { analogWrite(pin_pwm, constrain(blynk_v5 - 96, 77, 255)); json["pwm"] = constrain(blynk_v5 - 96, 77, 255); } else if (json["heatindex(c)"].as<double>() > 32 && json["heatindex-d(c)"].as<double>() > 0) { analogWrite(pin_pwm, constrain(blynk_v5 + 48, 77, 255)); json["pwm"] = constrain(blynk_v5 + 48, 77, 255); } else if (json["heatindex(c)"].as<double>() < 23 && json["heatindex-d(c)"].as<double>() > 2 && json["heatindex-d(c)"].as<double>() <= 4) { analogWrite(pin_pwm, constrain(blynk_v5 - 48, 77, 255)); json["pwm"] = constrain(blynk_v5 - 48, 77, 255); } else if (json["heatindex(c)"].as<double>() < 23 && json["heatindex-d(c)"].as<double>() > 4) { analogWrite(pin_pwm, constrain(blynk_v5 - 96, 77, 255)); json["pwm"] = constrain(blynk_v5 - 96, 77, 255); } else if (json["heatindex(c)"].as<double>() < 23 && json["heatindex-d(c)"].as<double>() < 0) { analogWrite(pin_pwm, constrain(blynk_v5 + 48, 77, 255)); json["pwm"] = constrain(blynk_v5 + 48, 77, 255); } else { analogWrite(pin_pwm, blynk_v5); json["pwm"] = blynk_v5; } } } void fn_loop_8640ms() { // get indoor discomfort index value from another device http.begin(client, "http://chanchunghoi.com/wemos/curl-bed-heatindex.php"); uint8_t httpResponseCode = http.GET(); String payload = ""; if (httpResponseCode > 0) { payload = http.getString(); if (payload != "") { json["discomfort-d(c)"] = cast_s2d(payload) - json["heatindex(c)"].as<double>(); } } http.end(); // send data to Blynk.cloud if (json["humidity(%)"].as<uint8_t>() > 0) { Blynk.virtualWrite(V1, json["heatindex(c)"].as<double>()); Blynk.virtualWrite(V2, json["temperature(c)"].as<double>()); Blynk.virtualWrite(V3, json["humidity(%)"].as<double>()); } }
Chrome 或者 Edge browser 已經內置相當唔錯嘅 IDE 功能,而 VS Code 應該係現時 interpreted language 嘅最佳 IDE。AJAX 令 browser 可以以大約一秒 refresh rate 速度不斷 update 網頁,所以 Wemos D1 都係用 1000ms loop 每隔一秒輸出 JSON,同時用 8640ms loop 向 Blynk.cloud 輸出数据制作歷史圖表。
Reference: