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: