/** * @file esp8266_iot_sample.ino * @brief 「格安デバイスで始めるIoT システム」の実装コード * @author DoSA (www.piffle.info) * @date 2019/11/30 */ #include #include #include #include #include #include #include #include #include #include #include #include #include // コンパイルエラーになる場合は、MQTTのバッファが短いです。 // ドキュメントディレクトリのArduinoフォルダ内にある、 // libraries/PubSubClient/src/PubSubClient.h で、 // MQTT_MAX_PACKET_SIZE を 512 以上に設定してください。 #if MQTT_MAX_PACKET_SIZE < 512 #error "MQTT_MAX_PACKET_SIZE is too small in PubSubClient.h" #endif /** *Pin割り当て定義 */ /// 設定リセット用のIO番号 #define RESET_IO_PIN 12 // [INPUT_PULLUP] HIGH : normal , LOW : reset /// 起動モード選択用のIO番号 #define RUNMODE_IO_PIN 14 // [INPUT_PULLUP] HIGH : run , LOW : config /** * 初期値定義 */ // 設定時に接続するWiFiの初期パスワードです. #define DEFAULT_ADMIN_PW "IoT@Tawagi" // NTPの初期アドレスです. // インターネットマルチフィード株式会社様の無償サービスを設定させていただいています. #define DEFAULT_NTP_SERV "ntp.jst.mfeed.ad.jp" // 起動間隔の初期値です. #define DEFAULT_INTERVAL 60 // sec /** * 結果状態の定義 */ #define STATUS_OK 0 // 正常 #define STATUS_BME_INIT_FAIL 100 // 異常:BME280の初期化 #define STATUS_WIFI_UP_FAIL 200 // 異常:WiFi接続の開始 #define STATUS_NTP_FAIL 300 // 異常:NTPによる時刻取得 #define STATUS_MQTT_CONNECT_FAIL 400 // 異常:MQTTの接続 #define STATUS_MQTT_PUBLISH_FAIL 401 // 異常:MQTTのメッセージ送信 /** * EEPROMに保存する設定の定義. */ #define CONF_VERSION 1 // CONFIG_STRUCTUREのバージョン #define CONF_LEN_NODE_NAME 64 // 以下いずれもNULL終端を含む長さ #define CONF_LEN_ADMIN_PW 64 #define CONF_LEN_WIFI_SSID 64 #define CONF_LEN_WIFI_PW 64 #define CONF_LEN_NTP_SERV 256 #define CONF_LEN_MQTT_SERV 256 #define CONF_LEN_MQTT_USER 64 #define CONF_LEN_MQTT_PW 64 #define CONF_LEN_MQTT_TOPIC 128 #define CONF_LEN_MQTT_FP 128 typedef struct { char header[2]; // 設定情報を識別するヘッダ uint16_t version; // 設定情報をのバージョン char nodeName[CONF_LEN_NODE_NAME]; // ノード名 char adminPw[CONF_LEN_ADMIN_PW]; // 設定画面のWiFiパスワード char wifiSSID[CONF_LEN_WIFI_SSID]; // 接続するWiFi基地局のSSID char wifiPw[CONF_LEN_WIFI_PW]; // 接続するWiFi基地局のパスワード char ntpServ[CONF_LEN_NTP_SERV]; // NTPサーバのアドレス char mqttServ[CONF_LEN_MQTT_SERV]; // MQTTサーバのアドレス:ポート char mqttUser[CONF_LEN_MQTT_USER]; // MQTTサーバのユーザ名 char mqttPw[CONF_LEN_MQTT_PW]; // MQTTサーバのパスワード char mqttTopic[CONF_LEN_MQTT_TOPIC]; // MQTTサーバのトピック char mqttFp[CONF_LEN_MQTT_FP]; // MQTTサーバの証明書Fingerprint uint32_t interval; // 起動間隔 int32_t status; // 最後の終了ステータス uint8_t checksum; // チェックサム (必ず最後に定義) } CONFIG_STRUCTURE; /** * グローバル変数. */ // モード共通 int32_t gStat; // 現在の実行結果状態 bool gIsResetMode; // 実行モードが設定リセットか否か bool gIsConfigMode; // 実行モードが設定モードか否か CONFIG_STRUCTURE gRomImg; // 設定情報を保持するバッファ CONFIG_STRUCTURE gRomTmp; // 設定情報の操作用のバッファ // 以下設定モード用 char gSSID[32]; // 基地局モードのSSID(最大31字NULL終端) IPAddress gApIp( 192, 168, 10, 1 ); // 基地局モードのIP IPAddress gApSubnet( 255, 255, 255, 0 ); // 基地局モードのIPのサブネット ESP8266WebServer gServer(80); // 基地局モードのHTTPサーバインスタンス // 以下通常モード用 Adafruit_BME280 gBme280; // BME280センサ用ドライバインスタンス WiFiClientSecure gWifiSec; // WiFiクライアントインスタンス(SSL/TLS用) PubSubClient gMqtt(gWifiSec); // MQTTクライアントインスタンス WiFiUDP gWifiUDP; // WiFiクライアントインスタンス(UDP用) /** * "アドレス:ポート"書式のアドレス部分のみを取得する. * @param (serverNamePort) 対象文字列へのポインタ * @return アドレス部分の文字列 */ String getServerNameSection(const char * serverNamePort) { String targetString(serverNamePort); int sepIndex = targetString.indexOf(':'); if (sepIndex < 0) { return targetString; } else { return targetString.substring(0, sepIndex); } } /** * "アドレス:ポート"書式のポート部分のみを取得する. * @param (serverNamePort) 対象文字列へのポインタ * @param (defalut) ポート部が無い場合のデフォルト値 * @return ポート部分の値 */ int getServerPortSection(const char * serverNamePort, int defalut) { String targetString(serverNamePort); int sepIndex = targetString.indexOf(':'); if (sepIndex < 0) { return defalut; } else { return targetString.substring(sepIndex+1).toInt(); } } /** * シリアル通信の初期化. */ void initSerial(){ Serial.begin(74880); // シリアルポート通信速度設定 Serial.println(); // 新しいラインにする } /** * CONFIG_STRUCTUREのチェックサムを計算する. * CONFIG_STRUCTUREのchecksumを除く全バイトのXORを計算します. * @return チェックサムの値. */ uint8_t calcCheckSum(CONFIG_STRUCTURE *pRomImg){ uint8_t* pIte = reinterpret_cast(pRomImg); uint8_t checksum = 0x00; for(; pIte < reinterpret_cast(&(pRomImg->checksum)) ; pIte++){ checksum ^= (*pIte); } return checksum; } /** * 現在のgRomImgの内容をEEPROMに書き込む. */ void saveRom(){ gRomImg.checksum = calcCheckSum(&gRomImg); EEPROM.put(0, gRomImg); EEPROM.commit(); } /** * gRomImgに初期値を設定し、EEPROMに書き込む. */ void resetRom(){ Serial.println("REST EEPROM: Start"); memset(&gRomImg, 0, sizeof(CONFIG_STRUCTURE)); gRomImg.header[0] = 'T'; gRomImg.header[1] = 'G'; gRomImg.version = CONF_VERSION; gRomImg.interval = DEFAULT_INTERVAL; strcpy(gRomImg.adminPw, DEFAULT_ADMIN_PW); strcpy(gRomImg.ntpServ, DEFAULT_NTP_SERV); saveRom(); Serial.println("REST EEPROM: Done"); } /** * 1. EEPROMを読み込みgRomImgにロードする. * 2. 正常値か確認し、異常のある時は初期化する. */ void initConfig(){ EEPROM.begin(sizeof(CONFIG_STRUCTURE)); // EEPROMの実行を開始 EEPROM.get(0, gRomImg);// EEPROM読み込み // ヘッダーとチェックサムを確認 bool isConfigured = ( gRomImg.header[0] == 'T' && gRomImg.header[1] == 'G' && gRomImg.version == CONF_VERSION && gRomImg.checksum == calcCheckSum(&gRomImg)); if(isConfigured == false){ resetRom(); } } /** * 設定画面の設定項目1行分を作成する. * 項目名単位等 * @param (name) 項目名 * @param (key) POST送信の際のキー名 * @param (value) 入力欄の初期値 * @param (suffix) 単位等 * @return タグに相当する文字列 */ String trInput(const char* name, const char* key, const char* value, short length, const char* suffix=""){ return String("") + ""+name+" : " + "" + suffix+""; } /** * Web画面の内容以外の共通部分で内容を修飾する. * @param (contents) ページ内容のHTML文字列 * @return タグに相当する文字列 */ String makeHtml(const char* contents){ return String("") + "" + "TAWA IoT Config" + "" + "" + "" + "" + "

TAWA IoT Config

" + "

"+gSSID+"

" + contents + "" + ""; } /** * 設定画面を生成する. * @return 設定画面に相当するHTML文字列 */ String configHtml(){ String contents = String("") + "
" + "" + trInput("Node Name","name",gRomImg.nodeName,32) + trInput("Admin PW","adminpw",gRomImg.adminPw,32) + trInput("WiFi SSID","ssid",gRomImg.wifiSSID,32) + trInput("WiFi PW","wifipw",gRomImg.wifiPw,32) + trInput("NTP Server","ntpsv",gRomImg.ntpServ,32) + trInput("MQTT Server","mqttsv",gRomImg.mqttServ,32) + trInput("MQTT User","mqttuser",gRomImg.mqttUser,32) + trInput("MQTT PW","mqttpw",gRomImg.mqttPw,32) + trInput("MQTT Topic","mqtttp",gRomImg.mqttTopic,32) + trInput("MQTT Fingerprint","mqttfp",gRomImg.mqttFp,32) + trInput("Interval","interval", String(gRomImg.interval).c_str(),16,"[Sec]") + "
" + "" + "
" + "
Last Status = " + gRomImg.status + "
"; return makeHtml(contents.c_str()); } /** * エラー画面を生成する. * @param (message) エラーメッセージ文字列. * @return エラー画面の相当するHTML文字列 */ String errorHtml(const char* message){ String contents = String("

ERROR

") + "

" + message + "

" + "

home

"; return makeHtml(contents.c_str()); } /** * 結果画面を生成する. * @param (message) メッセージ文字列 * @return 結果画面に相当するHTML文字列 */ String messageHtml(const char* message){ String contents = String("") + "

" + message + "

" + "

home

"; return makeHtml(contents.c_str()); } /** * 設定画面WebサーバのルートパスGETの処理. */ void serverOnRoot() { gServer.send( 200, "text/html", configHtml().c_str()); } /** * 設定画面のPOSTパラメータから文字列を安全にコピーする. * @param (pDest) 文字列のコピー先のポインタ * @param (argName) POSTパラメータのキー名 * @param (maxLen) pDestの最大の長さ * @return 正常にコピーできたか否か */ bool argcpy(char* pDest, const char *argName, uint32_t maxLen){ if(gServer.arg(argName).length() >= maxLen){ return false; // pDestのサイズを超えているのでエラー } memcpy(pDest, gServer.arg(argName).c_str(), gServer.arg(argName).length()+1); return true; } /** * 設定画面WebサーバのPOSTの処理. */ void serverOnPost() { bool bStat = true; //正常か否か // 各値を読み込む(可能な項目は読み込む) bStat=bStat && argcpy(gRomImg.nodeName , "name", CONF_LEN_NODE_NAME); bStat=bStat && argcpy(gRomImg.adminPw, "adminpw", CONF_LEN_ADMIN_PW); bStat=bStat && argcpy(gRomImg.wifiSSID , "ssid", CONF_LEN_WIFI_SSID); bStat=bStat && argcpy(gRomImg.wifiPw , "wifipw", CONF_LEN_WIFI_PW); bStat=bStat && argcpy(gRomImg.ntpServ , "ntpsv", CONF_LEN_NTP_SERV); bStat=bStat && argcpy(gRomImg.mqttServ , "mqttsv", CONF_LEN_MQTT_SERV); bStat=bStat && argcpy(gRomImg.mqttUser , "mqttuser", CONF_LEN_MQTT_USER); bStat=bStat && argcpy(gRomImg.mqttPw , "mqttpw", CONF_LEN_MQTT_PW); bStat=bStat && argcpy(gRomImg.mqttTopic , "mqtttp", CONF_LEN_MQTT_TOPIC); bStat=bStat && argcpy(gRomImg.mqttFp , "mqttfp", CONF_LEN_MQTT_FP); String argString= gServer.arg("interval"); if(argString.toInt() == 0 && argString.compareTo("0") != 0){ bStat=bStat && false; //Intに変換できない値が入っているのでエラー }else{ int argValue = argString.toInt(); if(argValue < 0 ) { bStat=bStat && false; //負値が入っているのでエラー }else{ gRomImg.interval=static_cast(argValue); } } if(bStat == false){ // 異常の場合はエラー画面を送信 gServer.send( 400, "text/html", errorHtml("Bad Request.").c_str()); }else{ // EEPROMへ書き込み saveRom(); // 成功画面を送信 gServer.send( 200, "text/html", messageHtml("SUCCESS! EEPROM configured.").c_str()); } } /** * 設定モードの初期化処理. * @return 処理の結果状態 */ int initModeConfig(){ Serial.println("INIT MODE: CONFIG"); // WiFiデバイスのMACアドレスを取得 byte mac_addr[6]; WiFi.macAddress(mac_addr); // MACアドレスからSSIDを作成します sprintf(gSSID, "TIoT-%02X%02X%02X%02X%02X%02X", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); Serial.print("CONFIG AP: SSID="); Serial.println(gSSID); // 基地局モードでWiFiを起動 WiFi.mode(WIFI_AP); WiFi.softAPConfig(gApIp, gApIp, gApSubnet); WiFi.softAP(gSSID, gRomImg.adminPw, (random(14) + 1)); Serial.print("CONFIG AP: IP="); Serial.println(WiFi.softAPIP()); // 各パスの処理内容を設定しWebサーバを起動 gServer.on("/", HTTP_GET, serverOnRoot); gServer.on("/post/", HTTP_POST, serverOnPost); gServer.begin(); } /** * 設定モードのloop処理. * @return 処理の結果状態 */ int loopModeConfig(){ gServer.handleClient(); return STATUS_OK; } /** * WiFiに接続する処理. * @return 処理の結果状態 */ int wifiUp(const char *psSsid, const char *psPskKey ) { Serial.print("WIFI UP: SSID=");Serial.println(psSsid); WiFi.mode(WIFI_STA); WiFi.begin(psSsid, psPskKey); while (WiFi.status() != WL_CONNECTED && WiFi.status() != WL_CONNECT_FAILED) { delay(500); Serial.print("."); } Serial.println(""); if (WiFi.status() == WL_CONNECTED) { Serial.print("WIFI UP: address="); Serial.println(WiFi.localIP()); Serial.println("WIFI UP: SUCCESS"); return STATUS_OK; } else { Serial.println("WIFI UP: FAILED"); return STATUS_WIFI_UP_FAIL; } } /** * NTPから取得したUNIX時間をISO8610形式の文字列に変換する. * ISO8610形式:"2019-01-02T13:14:15Z"のような文字列. * @param (epochtime) UNIX時間 * @return ISO8610形式の文字列 */ String epochtimeToISO8601(unsigned long epochtime) { char strbuf[20+1]; tmElements_t tme; breakTime(epochtime, tme); snprintf(strbuf, 20 + 1, "%04d-%02d-%02dT%02d:%02d:%02dZ", 1970+tme.Year, tme.Month, tme.Day, tme.Hour, tme.Minute, tme.Second); return String(strbuf); } /** * NTPに接続し現在時刻をISO8610形式の文字列で取得する. * @param (destString) 結果を受け取る文字列への参照 * @return 処理の結果状態 */ int getIso8601Time(String &destString){ char strbuf[64]; bool ntpUpdateSuccess = false; int ntpUpdateCount = 0; NTPClient ntpClient(gWifiUDP, gRomImg.ntpServ); ntpClient.begin(); while (( ntpUpdateSuccess = ntpClient.update() ) == false) { delay(1000); ntpUpdateCount++; if (ntpUpdateCount > 10) { Serial.println("Failed NTP Update."); return STATUS_NTP_FAIL; } } sprintf(strbuf, "%d", ntpClient.getEpochTime()); destString = epochtimeToISO8601(ntpClient.getEpochTime()); Serial.print("NTP: unixepoch="); Serial.println(strbuf); Serial.print("NTP: iso8601="); Serial.println(destString.c_str()); return STATUS_OK; } /** * BME280の初期化. * @return 処理の結果状態. */ int initBme280(){ unsigned status; status = gBme280.begin(); if (!status) { return STATUS_BME_INIT_FAIL; } return STATUS_OK; } /** * BME280で5回計測し、そのそれぞれの中央値を返す. * @param (pTemperature) 温度を格納する変数へのポインタ * @param (pHumidity) 湿度を格納する変数へのポインタ * @param (pPressure) 気圧を格納する変数へのポインタ * @return 処理の結果状態 */ int takeBme280(float* pTemperature, float* pHumidity, float* pPressure){ std::vector listTemp; std::vector listHumi; std::vector listPres; for (int count = 0; count < 5 ; count++){ gBme280.takeForcedMeasurement(); listTemp.push_back(gBme280.readTemperature()); listHumi.push_back(gBme280.readHumidity()); listPres.push_back(gBme280.readPressure()); delay(100); } std::sort(listTemp.begin(),listTemp.end()); std::sort(listHumi.begin(),listHumi.end()); std::sort(listPres.begin(),listPres.end()); *pTemperature = listTemp.at(2); *pHumidity = listHumi.at(2); *pPressure = listPres.at(2); return STATUS_OK; } /** * 文字列からJSON文字列を作成する. * @param (input) JSON文字列に変換する文字列へのポインタ. * @return JSON形式の文字列. */ String makeJson(const String &input){ String escapeTemp(input); escapeTemp.replace("\"","\\\""); return String("\"") + escapeTemp + "\""; } /** * std::mapからJSON文字列を作成する. * @param (input) JSON文字列に変換するmapへの参照 * @return JSON形式の文字列. */ String makeJson(const std::map &input){ String result = String("{"); char* nextSep = ""; auto ite = input.begin(); for (; ite != input.end(); ++ite){ result += nextSep; result += makeJson(ite->first); result += ":"; result += ite->second; nextSep = ","; } result += "}"; return result; } /** * MQTTの受信処理. */ void mqttCallback(const char* topic, byte* payload, unsigned int length) { // 利用しないので、シリアルにメッセージのみ残します. Serial.println("MQTT: Receive. (Unused)"); } /** * MQTTサーバへ接続する処理. * @return 処理の結果状態 */ int mqttConnect(){ int32_t stat = STATUS_OK; if (gMqtt.connected() == false) { Serial.println("MQTT: TRY CONNECT"); gWifiSec.setFingerprint(gRomImg.mqttFp); String mqttServer = getServerNameSection(gRomImg.mqttServ); int mqttPort = getServerPortSection(gRomImg.mqttServ, 8883); Serial.print("MQTT: SERVER="); Serial.println(mqttServer+":"+String(mqttPort)); Serial.print("MQTT: NODE=");Serial.println(gRomImg.nodeName); Serial.print("MQTT: TOPIC=");Serial.println(gRomImg.mqttTopic); Serial.print("MQTT: USER=");Serial.println(gRomImg.mqttUser); gMqtt.setServer(mqttServer.c_str(), mqttPort); gMqtt.setCallback(mqttCallback); if (gMqtt.connect(gRomImg.nodeName, gRomImg.mqttUser, gRomImg.mqttPw)) { Serial.println("MQTT: CONNECT SUCCESS"); } else { Serial.println("MQTT: CONNECT FAILED"); stat = STATUS_MQTT_CONNECT_FAIL; } } return stat; } /** * MQTTでメッセージを送信する. * @param (body) 送信する文字列への参照 * @return 処理の結果状態 */ int mqttPublish(String &body){ int32_t stat = STATUS_OK; if (gMqtt.connected() == true) { if (true == gMqtt.publish(gRomImg.mqttTopic, body.c_str())) { Serial.println("MQTT: PUBLISH SUCCESS"); } else { Serial.println("MQTT: PUBLISH FAILED"); stat = STATUS_MQTT_PUBLISH_FAIL; } } return stat; } /** * 通常モードの初期化処理. * @return 処理の結果状態 */ int initModeMain(){ int32_t stat = STATUS_OK; Serial.println("INIT MODE: MAIN"); //UP wifi if(stat == STATUS_OK){ stat = wifiUp(gRomImg.wifiSSID, gRomImg.wifiPw); } return stat; } /** * 通常モードのloop処理. * @return 処理の結果状態 */ int loopModeMain(){ int32_t stat = STATUS_OK; // NTPから時刻を取得 String isoTimeString; if(stat == 0){stat = getIso8601Time(isoTimeString);} // BME280で計測 float temperature=0.0, humidity=0.0, pressure=0.0; if(stat == 0){stat = takeBme280(&temperature, &humidity, &pressure);} String strTemperature = String(temperature, 2); String strHmidity = String(humidity, 2); String strPressure = String( pressure / 100.0F, 2); // 送信メッセージを作成 std::map bme280; std::map sensors; std::map result; bme280.insert(std::make_pair("temperature", makeJson(strTemperature))); bme280.insert(std::make_pair("humidity", makeJson(strHmidity))); bme280.insert(std::make_pair("pressure", makeJson(strPressure))); sensors.insert(std::make_pair("BME280", makeJson(bme280))); result.insert(std::make_pair("name", makeJson(gRomImg.nodeName))); result.insert(std::make_pair("timestamp", makeJson(isoTimeString))); result.insert(std::make_pair("sensors", makeJson(sensors))); String mqttBody = makeJson(result); // MQTTで送信 if (stat == STATUS_OK) { stat = mqttConnect(); } if (stat == STATUS_OK) { stat = mqttPublish(mqttBody); } // intervalの設定があれば、MQTTの接続断とDeepSleepの設定をする if(gRomImg.interval > 0){ if (gMqtt.connected() == true){ gMqtt.disconnect(); } ESP.deepSleep(gRomImg.interval*1000*1000); // [μsec] }else{ delay(1000); // [msec] DeepSleepしない場合の計測間隔 } return stat; } /** * 初期化処理. */ void setup() { gStat = STATUS_OK; // スイッチになっているピンのモードを設定 pinMode(RESET_IO_PIN, INPUT_PULLUP); pinMode(RUNMODE_IO_PIN, INPUT_PULLUP); // スイッチの状態を読み込み、モードを取得 gIsResetMode = (digitalRead(RESET_IO_PIN) == LOW); gIsConfigMode = (digitalRead(RUNMODE_IO_PIN) == LOW || gIsResetMode); // シリアルおよびコンフィグの初期化 initSerial(); initConfig(); // モード別の初期化処理 if (gIsConfigMode == true ) { if(gIsResetMode == true){resetRom();} if(gStat == STATUS_OK){gStat = initModeConfig();} }else{ if(gStat == STATUS_OK){gStat = initModeMain();} if(gStat == STATUS_OK){gStat = initBme280();} } // シリアルに結果状態を出力 Serial.print("END SETUP: status = ");Serial.println(gStat); } /** * メインループ処理. */ void loop() { // モードによって処理を変える. if (gIsConfigMode == true ) { if(gStat == STATUS_OK){gStat = loopModeConfig();} } else { if(gStat == STATUS_OK){gStat = loopModeMain();} } // 結果状態が異なる場合はEEPROMに保存する if(gStat != gRomImg.status){ gRomImg.status = gStat; saveRom(); } // 異常時のみ結果状態を出力(正常時はログが多くなるので省略) if(gStat != STATUS_OK){ Serial.print("END LOOP: status = ");Serial.println(gStat); // 異常時はDeepSleepで時間を取ってからデバイスリセットで起動する ESP.deepSleep(10*1000*1000); // [μsec] } }