文章程式碼顯示

2018年2月9日 星期五

NodeMCU 教學 - 10:台幣 200元解決 以 NodeMCU 實作 地震預(示)警 警報器 IFTTT 上傳到 GOOGLE SHEET

平時沒警報可當一般的時鐘使用


警報發生時,顯示震央規模;以及手機定位得到的當地最大震度




整個製作費用以及物品清單如下

NodeMCU ... 120 元
串列七段顯示器 ... 50 元
LED 燈、蜂鳴器、BJT、電阻、杜邦線、麵包板、厚紙板 ... 約 30 元

由於氣象局的示警推播目前只開放給公司企業進行簽約申請,個人用戶不行,所以我們必須用其它的方式來進行。

大致的原理圖如下


首先當地震發生時氣象局會利用感測器進行資料的擷取,大約須花費 20 秒的時間計算出震央以及震度等等的資訊,若達到示警的必要則會將資料傳送給有跟氣象局簽約的公司企業。

而公司企業會透過手機 APP (例如 KNY台灣天氣,這是我下載了四五個地震示警 APP 後認為可靠度較高的一個) 將這個速報訊息推播到用戶的手機中


當手機接收到來自 KNY台灣天氣 的地震示警時,手機會跳出界面如下



本來我都是單純依靠這個 APP 來看這次的地震有多大,但有時我的手機會出現頁面卡住的狀況發生。幾次後我覺得在兵荒馬亂之際還要開手機很緊張的點開介面讓我有點頭大,更何況有時手機不在身邊,聽到通知音也不曉得現在是不是地震預警的提示音,還以為是 Gmail 來信,所以決定弄個實體的警報器來輔助這點的不足

本來我是想要利用解 APP 封包的方式來攔截 KNY台灣天氣 的地震示警,但無奈能力不足且這會搞得很複雜,就打消這這個念頭。

我決定轉個彎,讓 第三方的 APP 幫我做這件事情,也就是 IFTTT 

我發現當 KNY台灣天氣 發生地震示警時,不僅會彈出上圖的介面,也會在手機的通知欄中以 "警示文字訊息" 的方式出現,如下圖紅框處


IFTTT 手機版 有個功能,當指定的 APP 在通知欄彈出某個訊息時,可以依照該訊息之中的 "關鍵字" 來進行 "事件" 的觸發。

將 "關鍵字" 設為 "地震" ,所以當 KNY台灣天氣 發出地震示警時,很自然的 IFTTT 這個 APP 就會進行 "事件" 的觸發。

然而事件倒底是什麼? 這部分為了簡單化我就不提了,你只要知道這樣做的目的是為了讓 NodeMCU 可以 "看到" 地震示警所發出來的警示訊息(上圖紅框)就好了

為了讓 NodeMCU 可以接收到警示訊息,本來我的想法是用 NodeMCU 架伺服器,再透過 IFTTT 間接將其傳送到 NodeMCU 。理以當這樣的做法是最直觀的作法,但無奈我對於架站方面的知識薄弱,且要申請固定 IP 也是一件麻煩事,所以我決定用 Google sheets 這個雲端的 Excel 來當做我的伺服器。

而 NodeMCU 做的事就是不斷的從 Google sheets 中去擷取資料,如此一來就可以得到手機彈出來的那串警示訊息了。

上述這些文字敘述就是我這個應用的原理,缺點是資料在雲端傳來傳去,會有一些延遲的問題,主要的延遲發生在 IFTTT 監看 KNY台灣天氣不夠即時,最久會延遲到 8 秒左右(狀況好大約延遲 2~3 秒),也就是手機收到示警後,最久要等到 10 秒才會傳到 NodeMCU ,此時已經距離地震發生 40 秒

也就是這個應用的預警盲區大概是震央的 140 公里左右

本人不會寫 APP ,若有人願意實現一個更即時擷取通知欄訊息並且傳遞字串到 google sheet 請與我聯絡,至少可以讓預警時間縮短3~10秒左右。

第二個缺點在於這個示警方法成立的前提條件是手機有收到 KNY台灣天氣的 地震示警,若手機沒收到,自然不會被 IFTTT 監看擷取,也不會傳送到 Google sheets ,NodeMCU 也抓不到什麼碗糕

這幾天花蓮地震,實測下來 KNY台灣天氣 的示警成功率大概有九成以上。

註 : 本人沒有做任何地震"預測" ,純粹是將氣象局發出來的地震示警資料做進一步運用


實作步驟如下

SETP1.

用電腦打開 Google 雲端硬碟 

STEP2. 

在雲端硬碟內新增一個資料夾名為 "IFTTT" ,接著在 IFTTT 資料夾內再新增一個資料夾名為 "Android notifications" ,接著再新增一個資料夾名為 "KNY"

然後在 KNY 裡面新增一個空白的 Google sheet ,名為 "EQ" (我的因為目前運作中,所以有文字)


注意檔案路徑必須正確

STEP3.

雙擊進入 EQ 

在 A1 儲存格複製貼上 "花蓮縣壽豐鄉 發生規模 4.6 地震本地預計1級, 震波在3秒後抵達. February 09, 2018 at 10:14AM"
在 A2 儲存格輸入 =MID(A1,FIND("模",A1)+2,3)
在 A3 儲存格輸入 =MID(A1,FIND("級",A1)-1,1)
在 A4 儲存格輸入 =LEFT(RIGHT(A1,4),2)


KNY 發送的通知訊息改變(2018/02/22) 改使用以下

在 A1 儲存格複製貼上 "地震速報:發生規模 5.4 地震.座標(23.5,121.5) , 深度: 20.0 KM February 22, 2018 at 07:11AM"
在 A2 儲存格輸入 =MID(A1,FIND("模",A1)+2,3)
在 A3 儲存格輸入 =MID(A1,FIND("度:",A1)+3,FIND("KM",A1) - FIND("度:",A1) -4)
在 A4 儲存格輸入 =LEFT(RIGHT(A1,4),2)

最後你應該得到下圖的結果,這部分一定要成功



其中 A2 儲存格代表震央強度;A3 表示你藉由手機定位得到的所在地最大震度;A3 表示該地震發生時的分鐘數

其中 A2 儲存格代表震央強度;A3 表示地震深度;A3 表示該地震發生時的分鐘數

STEP4.

從網址列找出你的 spreadsheetId


反藍處的那串先複製到文字文件備用(黑色是我將我的 spreadsheetId 遮蔽了)

STEP5.

"用手機" 至 Google Play 下載 IFTTT APP  並申請 IFTTT 帳號

STEP6.

由左至右點選紅框處



STEP7.

由左至右點選紅框處



STEP8.

由左至右點選紅框處



STEP9.



STEP10.




STEP11.

接下來就是搞 NodeMCU 的部分了,有關 NodeMCU 如何無痛用 Arduino 進行編譯,請見


硬體部分很簡單,普通 LED 接在 I/O 腳我就不說了。蜂鳴器因為 NodeMCU 的 I/O Output 為 3.3 V ,我希望可以用 5V 驅動,所以用一個簡單的 BJT 來做電位轉換。

電路請參考


Rc = 10 ohm 、 Rb = 470 ohm 。

我在 BC 之間加了一個陶瓷電容 104 解決在 Vb = 0 的狀況下風明器依然會微弱嗡嗡叫的問題,其原理 ... 我也不是很能說清楚,望有人能在留言區進行指點

串列顯示部分請查詢 PT6961 七段顯示,且在程式中有註解該如何接



STEP12.

所需要的 Library 我全部打包在這了,下載下來全部丟到 Arduino 的 libraries 資料夾內就好

註 : 因為七段顯示器在沒有地震警示時我是做為時鐘使用,其時間的來源為 BLYNK 。所以你還必須要搞一下 BLYNK 的部分。見下兩篇



當然,若你不需要時鐘功能也沒關係,就把程式內有關時間的部分都註解掉就好

STEP13.

接好 NodeMCU 的電路,安裝好 Arduino 開發環境, llibrary 也丟了,就直接複製以下的程式碼後燒錄,就可以動了

#include "ESP8266WiFi.h"
#include "BlynkSimpleEsp8266.h"
#include "ArduinoJson.h"
#include "SimpleTimer.h"
#include "OasisLED.h"
#include "WidgetRTC.h"


bool get_EQ();
int alarm_EQ();
bool connectWiFi_polling(const char* ssid, const char* pass, int tryNumber);
void alarm_blink(unsigned long ON_time, unsigned long OFF_time, int times);

// ===== Wifi setup =====
const char *ssid1 = "WIFI帳號";
const char *pass1 = "WIFI密碼";


// =====  EQ. setup =====
const char    *spreadsheetId = "在SEEP4你複製到文字文件的spreadsheetId";
const char    *EQhost = "spreadsheets.google.com";
char          EQrespBuf[1024*8];
static float  EQ_Center = 0;
static int    EQ_Local = 0;
static int    Mins = 0;
static int    Mins_last = 0;
SimpleTimer   timer;
SimpleTimer   timer_alarm;
int           timer_id,timer_id1;

// ==== BLYNK ====
char auth[] = "你的BLYNK KEY";
WidgetRTC rtc;

// ===== Hardware setup =====
const int buzzerPin = D1;
const int ledPin = D2;
const int dataPin = D6;
const int clkPin = D7;
const int csPin = D8;
OasisLED ledDisplay = OasisLED(clkPin, csPin, dataPin);

/* =====================*/
// === Alarm_Level_1 ===
/* =====================*/
void Alarm_Level_1(){
  static int cnt = 0;
  
  Serial.println("Alarm_Level_1.");
  digitalWrite(buzzerPin,!HIGH);   // BUZZER ALWAYS ON
    
  if ( cnt < 1119 ){
    digitalWrite(ledPin,!digitalRead(ledPin)); //blink fast
    cnt ++;
  }
  else
  {
    cnt = 0;
    digitalWrite(ledPin,LOW);       // LED OFF
    digitalWrite(buzzerPin,!LOW);   // BUZZER OFF
    //ledDisplay.reset(); //顯示清除
    timer.restartTimer(timer_id);
    timer.enable(timer_id);    
  }  
}

/* =====================*/
// === Alarm_Level_2 ===
/* =====================*/
void Alarm_Level_2(){
  static int cnt = 0;
  Serial.println("Alarm_Level_2.");
  
  alarm_blink(70, 70, 5); 
  delay(300);
  
  if( cnt < 49 ){    
    cnt ++;
  }
  else
  {
    cnt = 0;
    digitalWrite(ledPin,LOW);       // LED OFF
    digitalWrite(buzzerPin,!LOW);   // BUZZER OFF
    //ledDisplay.reset(); //顯示清除
    timer.restartTimer(timer_id);
    timer.enable(timer_id);    
  }  
}

/* =====================*/
// === Alarm_Level_3 ===
/* =====================*/
void Alarm_Level_3(){
  static int cnt = 0;
  Serial.println("Alarm_Level_3.");
  
  alarm_blink(80, 80, 3); 
  delay(520);
  
  if( cnt < 29 ){        
    cnt ++;
  }
  else
  {
    cnt = 0;
    digitalWrite(ledPin,LOW);       // LED OFF
    digitalWrite(buzzerPin,!LOW);   // BUZZER OFF
    //ledDisplay.reset(); //顯示清除
    timer.restartTimer(timer_id);
    timer.enable(timer_id);    
  }  
}

/* =====================*/
// === Alarm_Level_4 ===
/* =====================*/
void Alarm_Level_4(){
  static int cnt = 0;
  Serial.println("Alarm_Level_4.");
  
  alarm_blink(200, 100, 2); 
  delay(400);
  
  if( cnt < 7 ){    
    cnt ++;
  }
  else
  {
    cnt = 0;
    digitalWrite(ledPin,LOW);       // LED OFF
    digitalWrite(buzzerPin,!LOW);   // BUZZER OFF
    ledDisplay.reset(); //顯示清除
    timer_60000();
    timer.restartTimer(timer_id);
    timer.enable(timer_id);    
  }  
}

/* =====================*/
// === Alarm_Level_5 ===
/* =====================*/
void Alarm_Level_5(){
  static int cnt = 0;
  Serial.println("Alarm_Level_5.");
  
  alarm_blink(300, 700, 1);
  
  if( cnt < 3 ){
    cnt ++;
  }
  else
  {
    cnt = 0;
    digitalWrite(ledPin,LOW);       // LED OFF
    digitalWrite(buzzerPin,!LOW);   // BUZZER OFF
    ledDisplay.reset(); //顯示清除
    timer_60000();
    timer.restartTimer(timer_id);
    timer.enable(timer_id);    
  }  
}

/* =====================*/
// ===== timer_1800 ====
/* =====================*/
void timer_1800(){
  ledDisplay.toggleColon();
  if ( get_EQ() ){
      int level = alarm_EQ();
      Mins_last = Mins;
      if ( level ){
        timer.disable(timer_id);
        digitalWrite(ledPin,HIGH); // LED ON
        digitalWrite(buzzerPin,!HIGH);
        ledDisplay.reset(); //顯示清除
        ledDisplay.setBrightness(7); //設定亮度(0~7) 0最暗
        ledDisplay.setDigit(0, (int)(EQ_Center/1));
        ledDisplay.setDigit(1, (int)(EQ_Center*10)%10);
        ledDisplay.setDigit(3, EQ_Local);
      }
      switch(level)
      {
        case 1:
              Serial.println("Alarm Level 1 ");
              timer_alarm.setTimer(50, Alarm_Level_1, 1119);//共六十秒(恆長)
              break;
        case 2:
              Serial.println("Alarm Level 2 ");
              timer_alarm.setTimer(50, Alarm_Level_2, 50); //共五十秒
              break;
        case 3:
              Serial.println("Alarm Level 3 ");
              timer_alarm.setTimer(50, Alarm_Level_3, 30); //共三十秒
              break;
        case 4:
              Serial.println("Alarm Level 4 ");
              timer_alarm.setTimer(50, Alarm_Level_4, 8); //共八秒(短-長音)
              break;
        case 5:
              Serial.println("Alarm Level 5 ");
              timer_alarm.setTimer(50, Alarm_Level_5, 4); //共四秒(短-長音)(300+700)
              break;
        case 0:
              Serial.println("It's OK. No Alarm. "); // 確定沒事,可以做點雜事                
              break;
        default:
              Serial.println("default error");
              break;
      }// end switch
  }
  else 
  {
    Serial.println("Final Error");
    return;
  }
}

/* =====================*/
// ===== timer_60000 ====
/* =====================*/
void timer_60000(){
  byte h = hour();
  byte m = minute();
  ledDisplay.reset();
  int real_time = 100*h + m;
  ledDisplay.setValue(real_time);
  ledDisplay.setBrightness(0);
}

/* =====================*/
// ===== setup ====
/* =====================*/
void setup() 
{
  ESP.wdtDisable();
  ESP.wdtEnable(WDTO_1S);
  pinMode(ledPin, OUTPUT);
  pinMode(buzzerPin, OUTPUT);
  Serial.begin(115200);
  digitalWrite(ledPin,LOW);       // LED OFF
  digitalWrite(buzzerPin,!LOW);   // BUZZER OFF
  
  ledDisplay.initialize(); //初使化
  ledDisplay.setSpinnerMode(SPIN_NONE);
  ledDisplay.setBrightness(0); //設定亮度(0~7) 0最暗
    
  while (WiFi.status() != WL_CONNECTED)
  {
    if( connectWiFi_polling(ssid1,pass1,25) ) break;
  }
  
  Serial.println();
  Serial.print("Connected, IP address: ");
  Serial.println(WiFi.localIP());
  digitalWrite(ledPin,HIGH);
  digitalWrite(buzzerPin,!HIGH); 
  delay(20);
  digitalWrite(ledPin,LOW);       // LED OFF
  digitalWrite(buzzerPin,!LOW);   // BUZZER OFF

  get_EQ(); 
  Mins_last = Mins; // 取得在 google sheet 上,目前的資料。避免開機就觸發 Alarm
  
  Blynk.config(auth); //輸入Auth,顯示 BLYNK on Arduino 歡迎訊息
  Blynk.connect(); // 連線至 BLYNK ,成功後會顯示 Ready
  rtc.begin();
  while( !minute() )  {Blynk.run();} //等待時間同步
  timer_60000();

  timer_id = timer.setInterval(1800L, timer_1800);
  timer.setInterval(60000L, timer_60000);
  Serial.print("timer_id : ");
  Serial.println(timer_id);
  Serial.print("State : ");
  Serial.print(timer.isEnabled(timer_id));
}

/* =====================*/
// ===== loop ====
/* =====================*/
void loop()
{
  timer.run();
  timer_alarm.run();  
}

/* =====================*/
// ===== get_EQ ====
/* =====================*/
bool get_EQ()
{
  WiFiClient client;

  Serial.print("\nConnecting to ");
  Serial.println(EQhost);
  if (!client.connect(EQhost, 80)) {
    Serial.println("Connection failed");
    return false ;
  }
  
  // ====== Create a URI for the request =====
  String url = "/feeds/cells/";
  url += spreadsheetId;
  url += "/1/public/values?alt=json&range=A2:A4";
  
  //Serial.print("Requesting URL : ");
  //Serial.println(url);
  
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + EQhost + "\r\n" + 
               "Connection: close\r\n\r\n");
  client.flush();
  
  int EQrespLen = 0;
  bool skip_headers = true;
  
  while (client.connected() || client.available()) {
    if (skip_headers) {
      String aLine = client.readStringUntil('\n');
      if (aLine.length() <= 1) {
        skip_headers = false;
      }
    }
    else {
      int bytesIn;
      bytesIn = client.read((uint8_t *)&EQrespBuf[EQrespLen], sizeof(EQrespBuf) - EQrespLen);
      //Serial.print(F("bytesIn ")); Serial.println(bytesIn);
      if (bytesIn > 0) {
        EQrespLen += bytesIn;
        if (EQrespLen > sizeof(EQrespBuf)) EQrespLen = sizeof(EQrespBuf);
      }
      else if (bytesIn < 0) {
        Serial.print(F("read error "));
        Serial.println(bytesIn);
        return false;
      }
    }
    delay(10);
  }
  
  //Serial.println("\nClosing connection");
  client.stop();

  if (EQrespLen >= sizeof(EQrespBuf)) {
    Serial.print(F("EQrespBuf overflow "));
    Serial.println(EQrespLen);
    return false;
  }
  
  // Terminate the C string
  EQrespBuf[EQrespLen++] = '\0';
  //Serial.print(F("EQrespLen "));
  //Serial.println(EQrespLen);

  StaticJsonBuffer<1024> jsonBuffer;
  
  char *jsonstart = strchr(EQrespBuf, '{');
  if (jsonstart == NULL) {
    Serial.println(F("JSON data missing"));
    return false;
  }

  //Serial.println("\njsonstart");
  //Serial.println(jsonstart);

  JsonObject& root = jsonBuffer.parseObject(jsonstart);
  if (!root.success()) {
    Serial.println(F("jsonBuffer.parseObject() failed"));
    return false;
  }
  
  EQ_Center   = root["feed"]["entry"][0]["content"]["$t"];
  EQ_Local    = root["feed"]["entry"][1]["content"]["$t"];
  Mins        = root["feed"]["entry"][2]["content"]["$t"];
  
  Serial.print("EQ_Center = ");
  Serial.println(EQ_Center);
  Serial.print("EQ_Local = ");
  Serial.println(EQ_Local);
  Serial.print("EQ_mins = ");
  Serial.println(Mins);
  Serial.print("EQ_last_mins = ");
  Serial.println(Mins_last);
   
  return true;
}

/* =====================*/
// == connectWiFi_polling ==
/* =====================*/
bool connectWiFi_polling(const char* ssid, const char* pass, int tryNumber){
    
    int _try = 0;
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid,pass);
    Serial.print("Connecting to ");
    Serial.println(ssid);
    
    while( WiFi.status() != WL_CONNECTED ){
      if( _try <= tryNumber){
        delay(500);
        _try++;
        Serial.print(_try);
      }
      else {Serial.println("Connected Failed");  return false; }
    }
    return true;
  }
/* =====================*/
// ====== alarm_EQ  ======
/* =====================*/
int alarm_EQ(){
    if ( Mins != Mins_last ) {
      if ( EQ_Center >= 6 || EQ_Local >= 5 )                  return 1; //(震央特大)
      else if ( EQ_Center >= 5.2 && EQ_Local >= 4 )           return 2; //(震央大;本地中等)
      else if ( EQ_Center >= 5.2 && EQ_Local >= 2 )           return 3; //(震央大;本地小)
      else if ( EQ_Center >= 4.2 && EQ_Local >= 3 )           return 4; //(震央中等;本地中等)
      else if ( EQ_Center >= 4.2 && EQ_Local >= 1 )           return 5; //(震央中等;本地小)
      else return 0;      
    }//end if
    else return 0;
}

void alarm_blink(unsigned long ON_time, unsigned long OFF_time, int times){
  while(times){
    digitalWrite(ledPin,HIGH);       // LED ON
    digitalWrite(buzzerPin,!HIGH);   // BUZZER ON
    delay(ON_time);
    digitalWrite(ledPin,LOW);       // LED OFF
    digitalWrite(buzzerPin,!LOW);   // BUZZER OFF
    delay(OFF_time);
    times--;
  }
}



最後,實際作動的影片連結在此

↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引