2016年7月21日 星期四

滷苦瓜的方法

這陣子中午常到公司附近的國泰民安素食店吃午飯, 只要有滷苦瓜必點, 因為廚師將苦瓜滷得甘甜軟嫩, 吃後令人齒頰留香, 在這炎炎夏日最為退火. 有次我點完菜, 跟老闆誇讚其苦瓜滷得好, 順勢問她是怎麼滷的, 怎麼這麼好吃? 她只說是用豆薣加一些香料去滷, 原來是用豆薣啊! 但是甚麼香料咧? 或許是人家的秘密, 我就不方便再問了.

上週回鄉下滷苦瓜時就找出媽生前所製作的最後一罐豆薣, 舀了兩湯匙加進去, 另外加兩湯匙冰糖, 以及適量醬油, 水加至八分滿去滷, 結果吃起來有比較好吃, 苦味降低了, 但還不到素食店老闆的水準. 今天查了網路, 發現下列這個網站有非常多的苦瓜料理 :

# 苦瓜料理食譜作法

我挑了其中幾個還不錯的苦瓜食譜如下 :

# 油燜苦瓜香菇
# 豆鼓油悶苦瓜
# 苦瓜封
# 苦瓜燉蔭瓜雞湯
# 蔭瓜滷苦瓜
# 甘味苦瓜排骨
# 甘甜苦瓜排骨湯
# 百香果苦瓜泡菜
# 涼拌脆苦瓜
# 醬鳳梨釀苦瓜一鍋搞定
# 醬燒苦瓜

綜合上面幾個食譜, 我猜素食店老闆的滷苦瓜好吃的秘訣應該是豆薣加蔭瓜! 而她所說的香料有可能是八角. 上面食譜中也有人放香菇與蠔油下去, 因此這周回鄉下滷苦瓜時我打算放入下列八項去調味看看 :

八角, 蔭瓜, 豆薣, 香菇, 蠔油, 冰糖

另外食譜中有一些關於苦瓜的常識, 整理如下 :
  1. 苦瓜越白越不苦;偏白(脆)適合涼拌;偏黃(糯)適合紅燒。
  2. 苦瓜挑選時,表面凸起顆粒較大的,苦瓜越不苦。
  3. 百香果加砂糖,很容易在濾網中攪拌去籽,做成冰塊保存,打果汁或涼拌都好用。
  4. 苦瓜苦味來自果肉的地方並非白色薄膜。一般料理都會說要去除白色薄膜是因為口感不佳無脆度,白色海綿膜咬起來很有韌性口感不佳,相反的果肉是脆也是苦味的來源,所以涼拌時要以泡水方式讓果肉釋放苦澀味。
苦瓜是葫蘆科蔬菜, 性寒, 在食療中有清熱降火解毒功效, 其維生素 C 含量是瓜類中最高的, 故有抗氧化功能. 現代醫學研究發現苦瓜萃取物可增強免疫力, 對愛滋病, 癌症, 糖尿病等疾病可能有療效, 目前還尚未充分確認. 苦瓜熱量低, 含有豐富膳食纖維, 且其中的植化素苦瓜苷具有降血糖作用, 因此適合糖尿病人食用. 不過苦瓜苷活性在高溫烹煮下會減弱, 冷凍處理後反而升高. 值得注意的是, 苦瓜含鉀量較高, 糖尿病有腎病變者不宜多食.

參考 :

找便宜的好工具 : 品購網

昨天下午公司樓下會客室有廠商來擺攤, 賣的是金健康 6D 循環透氣床墊, 同事力邀我下去看看 (他想買, 人多好殺價), 單人 1680, 雙人 2600, 我同事買了兩單一雙, 殺到 1600 與 2500, 廠商說外面買至少要多 1000 塊. 我看了之後也有點心動, 但生意嘴吼溜溜, 你若全信的話肯定吃悶虧. 我說考慮一下, 下班前再看看. 其實是想上樓查一下網路價格比價一下.

搜尋 "金健康 6D 循環透氣床墊" 找到這個品購網, 原來這是一個比價網站, 它會搜尋各大購物網站, 按價格由低到高排列, 就可以找出目前最便宜的價位.

# https://www.pingle.com.tw/

透過品購網找到 UDN 剛好昨天單人的有特價 88 折 1480 (1680*0.88), 便宜 200 元, 我就買了兩個, 另外一個給姊姊, 合計 2960, 加上 UDN 有定期贈送購物金, 昨天到期的有 600 元, 可使用 592 元, 因此只需付 2960-592=2368, 平均單價是 2368/2=1184 元, 比同事買的 1600 便宜了 416 元, 相當於原價打七折 (1184/1680). 果然驗證了生意嘴吼溜溜, 還說外面要貴上 1000 塊哩.

這告訴我們甚麼道理? 所謂的行銷術, 其實是一種騙術. 利用時空的限制 (你沒時間跑遠路貨比三家) 所造成的資訊不對稱, 靠一張嘴與誘人的話術, 讓消費者陷入自以為撿到便宜的陷阱, 產生強烈的購物衝動, 最終完成了經濟學上的供需滿足交易行為 (貨終於賣出去了 vs 今天買到 CP 值超高的東西).

對於業務員 (賣保險的, 賣童書的, 賣線上教學的 ... Any salespersons), 永遠要把他們當成潛在的騙子看. 我個人十幾年前幫小朋友買了一套線上學習教材, 業務員保證可無限期使用 (她會用員工帳號續約), 且公司會不定期派員訪視小朋友學習成果, 我竟然天真的相信了, 結果訪視是個屁, 三年後無法再使用, 打電話給當初的業務員, 她換手機了變空號, 打給公司, 公司說使用期限為三年 (契約書確實是寫三年), 無限期那是業務員個人行為亂保證. 我仔細審視了業務員的工作, 沒底薪, 只有業務獎金, 只要拉到生意, 這個月就有收入, 當然要竭盡所能+不擇手段做成一筆生意啦! 因此我得到一個結論 : 推銷員都是潛在的騙子, 但他們都是有交付貨品, 一切照合約走, 所以無法構成詐騙. 會得到這結論或許有點偏頗, 但我也是愛莫能助, 受到這種內傷, 不會偏頗才奇怪.

現在網路資訊發達, 消費者只要理性一點克制一下馬上消費的衝動, 回家查一下網路再決定也不遲. 現在智慧型手機更方便了, 用 App 也可以馬上查價比價, 例如這個品購網也可以下載手機 App, 逛街或有人兜售時可以隨時查核推銷員所言是否屬實 :

# Pingle省省吧 - 自動比價找便宜


2016年7月20日 星期三

關於 KOBOT 掃地機器人 RV337

今天上公司福利社網站, 赫然發現今年福利金是 10000 點, 早知道的話, 姊姊的手機用這來買就好啦! 這下不知該買甚麼, 就想起去年想買後來又打消念頭的掃地機器人, 但去年那款 LG 的已經沒在賣了, 現在只賣這款 KOBOT 的 RV337, 可掃地吸塵拖地, 還能返回充電站自動回充, 福利社定價 3990, 目前做活動加贈「TECHKO MAID聰明管家拖地機器人RM101」, 似乎蠻誘人的 :

# [ 開箱 ] 【美國 KOBOT】智慧多功能自動回充掃地機器人(銀色) [ 開箱 ]

製造商美國特科 Techko 公司是台灣人柯約瑟創辦, 參考 :

# 美特科集團正修結盟 研發機器人接軌世界

台灣總代理台擘在內湖, 電話:02-2799-0866 客服專線:02-2799-1969, 售後維修需寄回台北處理, 比較麻煩. 贈品 RM101 是拖地機器人, 沒有附電池, 須自己買 4 顆三號電池, 參考 :

# Techko Maid聰明管家拖地機器人RM101 吸塵器 $3000





打電話給代理商詢問功能, 摘要如下 :
  1. 此款掃地機無遙控器, 按面板上按鍵即開始掃地, 內有數種清掃模式會自動變換, 但無路徑記憶功能, 掃完會自動返回充電座.
  2. 贈品 RM101 拖地機器人使用 3 號電池四顆, 可連續使用約兩小時. 
看來此款只是一般低價位掃地機器人加上自動返航功能而已, 有路徑記憶功能的大都要上萬元. 而贈品似乎因為滯銷才搭配銷售. 總之, 有贈品促銷的物件要特別注意, 通常只是為了挑起不理性的購買衝動而已. 所以考量後決定暫時不買了. 有可能轉而購買新的汽車導航與行車紀錄器, 我那台 QRV 今年已滿十年, 原先安裝的照後鏡導航機已經太老舊, 且照後鏡塑膠柱已斷裂, 最近要找時間去附近店家問問看施工條件, 再來決定是否自購.


2016年7月19日 星期二

★ 使用 ESP8266 傳送 Twitter 訊息

我去年買了一塊 Wiznet5100 乙太網擴充板疊在老張借我的 Arduino UNO 板上, 進行過一次透過 Ethernet 傳送 Twitter 的實驗, 參考 :

# Arduino 網路測試 : 發送 Twitter

但是自從學會使用 ESP8266 讓 Arduino 上網之後, 這塊又貴又大 (比起 ESP8266) 的板子就連同 UNO 一起放在零件盒裡不見天日了. 能用 Wifi 上網幹嘛還要用需要拉網路線的乙太網呢?

今天在 Oreilly  "Arduino Cookbook 錦囊妙計" 這本書的 15.12 節又看到傳送訊息到 Twitter 的介紹, 那今晚就用 ESP8266 來重作這個實驗吧!

在上面 2015/4/2 的 Twitter 實驗中, 我是依照 "Arduino 自造指南" 這本書的作法, 透過 arduino-tweet 這個第三方網站所提供的函式庫來傳送訊息給 Twitter. 不過那個函式庫是為乙太網寫的, 沒辦法用在 ESP8266. 在歐萊里出版的 "Arduino Cookbook 錦囊妙計" 這本書裡的 15.12 節有介紹利用 Thingspeak 的 ThingTweet API 來當中繼站傳送 Tweet 訊息到自己的推特.

首先登入 Thingspeak 帳戶, 切換到 Apps 頁籤即可看到 ThingTweet 入口 :


點 "ThingTweet" 後按 "Link Twitter Account" 按鈕 :


按 "授權應用程式" 鈕 :


這樣就表示已授權 Thingspeak 傳送訊息到我的推特了.


按 "Back to ThingTweet" 就可以看到 API Key 了, 這個要記錄下來, 程式中要用到 :


接下來就可以撰寫程式, 讓 Arduino 利用 ESP8266 的 Wifi 連線功能向 Thingspeak 發出連線要求, 傳送 Tweet 訊息, 由 ThingTweet API 代為轉發到我們的推特上. 首先必須與 Thingspeak (網址 api.thingspeak.com) 建立 TCP 連線, 其 AP 指令如下 :

AT+CIPSTART="TCP","api.thingspeak.com",80

然後告訴 ESP8266 接下來要傳送的資料長度 :

AT+CIPSEND=字元數

最後傳送資料 (HTTP 內容), Thingspeak 網站的範例使用 HTTP 的 POST 方法, 下面這篇文章則是使用 GET 方法, 參考 :

# Beginner's guide to ESP8266 and tweeting using ESP8266 

這裡為求簡單也使用 GET 方法, 傳送的資源字串格式為 :

GET /apps/thingtweet/1/statuses/update?api_key=ThingTweet識別碼&status=要傳送的推特訊息

完整的程式如下 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
String ThingTweet_API="B2B4JF5WEUXGCOUY";
int i=0; //counter

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  }

void loop() {
  Serial.println("Connecting Thingspeak ...");
  sendData("AT+CIPSTART=\"TCP\",\"api.thingspeak.com\",80\r\n",1000,DEBUG);
  String data="GET /apps/thingtweet/1/statuses/update?api_key=" + ThingTweet_API + 
              "&status=Hello! " + i + " times.\r\n";
  sendData("AT+CIPSEND=" + (String)data.length() + "\r\n",1000,DEBUG); 
  String res=sendData(data,3000,DEBUG);  
  res.replace("\r\n",""); //remove all line terminator   
  if (res.indexOf("Unlink") == -1) { //if no auto unlink
    sendData("AT+CIPCLOSE\r\n",2000,DEBUG); //close session
    }
  delay(60000);
  i++;
  }

String sendData(String command, const int timeout, boolean debug) {
  String res="";
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

此程式中我使用了一個發送次數計數器來避免每次發文內容相同, 這是推特的限制之一, 即連續兩次推文不可雷同, 否則第二則推文不會發送成功. 另外推特還有一個限制, 就是一分鐘內只能發送一個推文, 因此我在 loop() 最底下用了 delay(60000) 讓程式到此停止一分鐘以滿足推特之限制. 此程式每分鐘會發出 "Hello! x times." 的推文到我的 Twitter 上. 另外, length() 傳回的是整數, 因此必須用 (String) 強制轉型.

程式上傳執行後, 我手機果然每分鐘收到推文了 :


串列埠監控視窗擷取訊息如下 :

AT+RST


OK

bB�鑭b禕S��"愃L�侒��餾�
[System Ready, Vendor:www.ai-think
192.168.43.40

OK
Connecting Thingspeak ...
AT+CIPSTART="TCP","api.thingspeak.com",80


OK
Linked
AT+CIPSEND=88

> GET /apps/thingtweet/1/statuses/update?api_key=B2B4JF5WEUXGCOUY
SEND OK

+IPD,1:1
OK

OK
Unlink


Connecting Thingspeak ...
AT+CIPSTART="TCP","api.thingspeak.com",80


OK
Linked
AT+CIPSEND=88

> GET /apps/thingtweet/1/statuses/update?api_key=B2B4JF5WEUXGCOUY
SEND OK

+IPD,1:1
OK

OK
Unlink

可見每次傳送完畢, Thingspeak 通常都會自動拆線, 但萬一沒有自動拆線的話, 就必須下 AT+CIPCLOSE 的指令強制拆線. 所以上面程式中對最後一次呼叫 sendData() 的傳回值進行字串處理, 若沒有找到 "Unlink"  字眼就強制拆線.

2016-07-27 補充 :

上面範例程式使用我自製的 IOT 模組, 已經事先切換到設定模式, 以手機設定好要連線之 AP 後, 再切回工作模式, 因 ESP8266 特性是除非寫入新的設定值, 否則它就保存最近一次的寫入值, 因此上面範例程式中不須使用 AT+CWJAP 指令去設定要連線之 AP.

如果要在程式中自行設定連線 AP 要怎麼做? 下列範例程式中我另外寫了一個 connectWifi() 函數, 只要傳入 ssid 與 pwd, 它會向 ESP8266 送出 AT+CWJAP 指令, 若回應中有 "OK", 就回傳 true, 否則 false.

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
String ssid="H30-L02-webbot";
String pwd="1234567890";
String ThingTweet_API="B2B4JF5WEUXGCOUY";
int i=0; //counter

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  while (!connectWifi(ssid, pwd)) {
    Serial.println("Connecting WiFi ... failed");
    delay(2000);
    }
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  }

void loop() {
  Serial.println("Connecting Thingspeak ...");
  sendData("AT+CIPSTART=\"TCP\",\"api.thingspeak.com\",80\r\n",1000,DEBUG);
  String data="GET /apps/thingtweet/1/statuses/update?api_key=" + ThingTweet_API +
              "&status=Hello! " + i + " times.\r\n";
  sendData("AT+CIPSEND=" + (String)data.length() + "\r\n",1000,DEBUG);
  String res=sendData(data,3000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("Unlink") == -1) { //if no auto unlink
    sendData("AT+CIPCLOSE\r\n",2000,DEBUG); //close session
    }
  delay(60000);
  i++;
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",8000,DEBUG); 
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}  
  }

String sendData(String command, const int timeout, boolean debug) {
  String res="";
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

在 setup() 中我們用 while 來檢查 connectWifi() 是否傳回 true, 是的話表示連線成功, 否則就一直印出連線失敗訊息, while 迴圈不會結束, 因此沒機會到 loop() 中執行 twitter 訊息之發布. 注意, 在 connectWifi() 函數中, AT+CWJAP 的等待時間若太短的話, 可能會一直無法連上基地台, 因為連線需要一些時間. 我之前用 6 秒, 發覺不是很穩定, 調成 8 秒後就沒問題了.

參考 :

# How to tweet from an Arduino using the wifi sheild
# 以 Arduino+ESP8266 物聯網模組重作 DHT11 溫溼度實驗
# How to tweet with your Arduino
# Twitter controlled Arduino Outputs - no PC - LCD Display + Sensor data to Twitter
# TweetControl: Control Anything with Twitter


2016年7月18日 星期一

利用 NTP 伺服器來同步 Arduino 系統時鐘 (二)

晚上檢視前天寫的程式, 發現 Time 函式庫還有一個 weekday() 函數可用來取得今日是星期幾, 它會傳回 1~7 的整數, 星期天是一周的起始傳回 1, 星期六傳回 7, 我們可以用一個陣列將 weekday() 的傳回值映照成 Sun, Mon, ... Sat 等字串, 以便顯示在 1602 LCD 上時更具有可讀性. 參考 :

# 利用 NTP 伺服器來同步 Arduino 系統時鐘 (一)

我把前一篇中測試 5 的自訂函式做了一番更動, 從 NTP 取回時間訊息經運算後傳回台灣時間的函式改名為 getCST(); 取得日期與時間字串的函式也拆成 getDate() 與 getTime(), 這樣比較有彈性. 然後寫了一個新函數 getWeek() 來轉換 weekday() 函數所傳回的星期代碼. 完整程式如下 :

測試 6 : 

#include <SoftwareSerial.h>
#define DEBUG true
#include <Time.h>
#include <TimeAlarms.h>
#include <LiquidCrystal.h>
#define RS 2
#define E 3
#define D4 10
#define D5 11
#define D6 12
#define D7 13

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP
LiquidCrystal lcd(RS,E,D4,D5,D6,D7);  //create LCD object

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  sync_clock();
  Alarm.timerRepeat(60, sync_clock);
  lcd.begin(16,2); //define 2*16 LCD
  lcd.clear(); //clear screen
  }

void loop() {
  String d=getDate();
  String t=getTime();
  String w=getWeek();
  Serial.println(d + " " + t + " " + w);
  lcd.setCursor(0,0); //move to (x,y)
  lcd.print(d); //print date
  lcd.setCursor(11,0); //move to (x,y)
  lcd.print(w); //print date  
  lcd.setCursor(0,1); //move to (x,y)
  lcd.print(t); //print date  
  Alarm.delay(1000);
  }

void sync_clock() {
  setTime(getCST());
  }

String getDate() {
  String d=(String)year() + "-";
  byte M=month();
  if (M < 10) {d.concat('0');}
  d.concat(M);
  d.concat('-');
  byte D=day();
  if (D < 10) {d.concat('0');}
  d.concat(D);
  return d;
  }

String getTime() {
  String t="";
  byte h=hour();
  if (h < 10) {t.concat('0');}
  t.concat(h);
  t.concat(':');
  byte m=minute();
  if (m < 10) {t.concat('0');}
  t.concat(m);
  t.concat(':');
  byte s=second();
  if (s < 10) {t.concat('0');}
  t.concat(s);
  return t;
  }

String getWeek() {
  String w[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
  return w[weekday()-1];
  }

long getCST() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  //132.163.4.101, 132.163.4.102, 132.163.4.103
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

這裡除了新增的 getWeek() 函數外, 其餘只是函數名稱更改與功能拆分而已. 分別呼叫 getDate() 與 getTime(), 以及 getWeek() 再去組合想要的輸出字串, 我覺得這樣比較好.

串列埠監控視窗擷取訊息如下 :

AT+RST


OK
bB�鑭b禔S��"��侒��S��
[Vendor:www.ai-thinker.com Version:0.9.2.4]

ready
AT+GMR

0018000902-AI03

OK
AT+CIFSR

192.168.2.101

OK
Sending request to NTP server ...
AT+CIPSTART="UDP","91.226.136.136",123


OK
AT+CIPSEND=48

>
NTP server answered :
E3 00 06 EC 00 00 00 00 00 00
00 00 31 4E 31 34 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 0D 0A
53 45 4E 44 20 4F 4B 0D 0A !!0D
0A 2B 49 50 44 2C 34 38 3A 24
02 06 ED 00 00 00 29 00 00 05
EF FA 3D D8 B4 DB 37 5B 76 94
DD 57 89 00 00 00 00 00 00 00
00 DB 37 5C 46 81 C8 A2 A7 DB
37 5C 46 81 F6 99 1F 0D 0A 4F
4B 0D

123 bytes received
NTP time stamp packets (byte 101~104)=DB 37 5C 46
NTP time stamp (seconds since 1900-01-01)=3677838406
Unix time stamp (seconds since 1970-01-01)=1468849606
AT+CIPCLOSE


OK
Unlink
2016-07-18 21:46:46 Mon
2016-07-18 21:46:47 Mon
2016-07-18 21:46:48 Mon
2016-07-18 21:46:49 Mon
2016-07-18 21:46:50 Mon
2016-07-18 21:46:51 Mon
2016-07-18 21:46:52 Mon
2016-07-18 21:46:53 Mon
2016-07-18 21:46:54 Mon
2016-07-18 21:46:55 Mon
2016-07-18 21:46:56 Mon
2016-07-18 21:46:57 Mon
2016-07-18 21:46:58 Mon
2016-07-18 21:46:59 Mon
2016-07-18 21:47:00 Mon
2016-07-18 21:47:01 Mon
2016-07-18 21:47:02 Mon
2016-07-18 21:47:03 Mon
2016-07-18 21:47:04 Mon
2016-07-18 21:47:05 Mon
2016-07-18 21:47:06 Mon
2016-07-18 21:47:07 Mon
2016-07-18 21:47:08 Mon
2016-07-18 21:47:09 Mon
2016-07-18 21:47:10 Mon
2016-07-18 21:47:11 Mon
2016-07-18 21:47:12 Mon
2016-07-18 21:47:13 Mon
2016-07-18 21:47:14 Mon
2016-07-18 21:47:15 Mon
2016-07-18 21:47:16 Mon
2016-07-18 21:47:17 Mon
2016-07-18 21:47:18 Mon
2016-07-18 21:47:19 Mon
2016-07-18 21:47:20 Mon
2016-07-18 21:47:21 Mon
2016-07-18 21:47:22 Mon
2016-07-18 21:47:23 Mon
2016-07-18 21:47:24 Mon
2016-07-18 21:47:25 Mon
2016-07-18 21:47:26 Mon
2016-07-18 21:47:27 Mon
2016-07-18 21:47:28 Mon
2016-07-18 21:47:29 Mon
2016-07-18 21:47:30 Mon
2016-07-18 21:47:31 Mon
2016-07-18 21:47:32 Mon
2016-07-18 21:47:33 Mon
2016-07-18 21:47:34 Mon
2016-07-18 21:47:35 Mon
2016-07-18 21:47:36 Mon
2016-07-18 21:47:37 Mon
2016-07-18 21:47:38 Mon
2016-07-18 21:47:39 Mon
2016-07-18 21:47:40 Mon
2016-07-18 21:47:41 Mon
2016-07-18 21:47:42 Mon
2016-07-18 21:47:43 Mon
2016-07-18 21:47:44 Mon
2016-07-18 21:47:45 Mon
AT+CIPSTART="UDP","91.226.136.136",123


OK
AT+CIPSEND=48

>
NTP server answered :
E3 00 06 EC 00 00 00 00 00 00
00 00 31 4E 31 34 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 0D 0A
53 45 4E 44 20 4F 4B 0D 0A !!0D
0A 2B 49 50 44 2C 34 38 3A 24
02 06 ED 00 00 00 29 00 00 06
33 FA 3D D8 B4 DB 37 5B 76 94
DD 57 89 00 00 00 00 00 00 00
00 DB 37 5C 8A DA 03 81 56 DB
37 5C 8A DA 0F A2 81 0D 0A 4F
4B 0D 0A

124 bytes received
NTP time stamp packets (byte 101~104)=DB 37 5C 8A
NTP time stamp (seconds since 1900-01-01)=3677838474
Unix time stamp (seconds since 1970-01-01)=1468849674
AT+CIPCLOSE


OK
Unlink
2016-07-18 21:47:54 Mon
2016-07-18 21:47:55 Mon
2016-07-18 21:47:56 Mon
2016-07-18 21:47:57 Mon
......


這樣加上星期資訊後感覺比較完整了.

但是, 如果能加上 DHT11 感測模組取得的溫濕度資訊就更完美了. 剛好 1602 LCD 顯示時間的第二排後面還有 8 個空白可用來顯示攝氏溫度與濕度, 位置分配如下 :

22:00:44 30° 59%

這樣剛剛好填滿第二排. 關於 DHT11 用法, 參考最近一次的實驗 :

# 以 Arduino+ESP8266 物聯網模組重作 DHT11 溫溼度實驗

在此溫濕度實驗中, 使用了 D2 腳作為讀取 DHT11 資料輸出之用, 但因為 1602 使用了 D2 連接其 RS 腳, 因此 DHT11 輸入我另外找了 D6 來讀取. 注意, 下面的程式需要到 Github 下載 Adafruit 的 DHT11/22 函式庫到 Arduino 安裝目錄下的 library 資料夾下 (然後 Arduino IDE 必須重開才抓得到) :

# adafruit/DHT-sensor-library

測試 7 :

#include <SoftwareSerial.h>
#define DEBUG true

#include <Time.h>
#include <TimeAlarms.h>
#include <LiquidCrystal.h>
#define RS 2
#define E 3
#define D4 10
#define D5 11
#define D6 12
#define D7 13

#include "DHT.h"
#define DHTPIN 6   
#define DHTTYPE DHT11

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP
LiquidCrystal lcd(RS,E,D4,D5,D6,D7);  //create LCD object
DHT dht(DHTPIN, DHTTYPE); // Initialize DHT sensor

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  sync_clock();
  Alarm.timerRepeat(60, sync_clock);
  lcd.begin(16,2); //define 2*16 LCD
  lcd.clear(); //clear screen
  }

void loop() {
  String d=getDate();
  String t=getTime();
  String w=getWeek();
  Serial.println(d + " " + t + " " + w);
  lcd.setCursor(0,0); //move to (x,y)
  lcd.print(d); //print date
  lcd.setCursor(11,0); //move to (x,y)
  lcd.print(w); //print date  
  lcd.setCursor(0,1); //move to (x,y)
  lcd.print(t); //print date  
  float h=dht.readHumidity();        //get humidity from DHT11
  float c=dht.readTemperature();     //get temperature (C) from DHT11 
  if (isnan(h) || isnan(c)) {h=0.0;c=0.0;}
  lcd.setCursor(9,1); //move to (x,y)
  lcd.print(int(c)); //print celcius  
  lcd.setCursor(11,1); //move to (x,y)
  lcd.print((char)223); //print degree symbol
  lcd.setCursor(12,1); //move to (x,y)
  lcd.print(' '); //print space   
  lcd.setCursor(13,1); //move to (x,y)
  lcd.print(int(h)); //print humidity 
  lcd.setCursor(15,1); //move to (x,y)
  lcd.print('%'); //print %      
  Alarm.delay(1000);
  }

void sync_clock() {
  setTime(getCST());
  }

String getDate() {
  String d=(String)year() + "-";
  byte M=month();
  if (M < 10) {d.concat('0');}
  d.concat(M);
  d.concat('-');
  byte D=day();
  if (D < 10) {d.concat('0');}
  d.concat(D);
  return d;
  }

String getTime() {
  String t="";
  byte h=hour();
  if (h < 10) {t.concat('0');}
  t.concat(h);
  t.concat(':');
  byte m=minute();
  if (m < 10) {t.concat('0');}
  t.concat(m);
  t.concat(':');
  byte s=second();
  if (s < 10) {t.concat('0');}
  t.concat(s);
  return t;
  }

String getWeek() {
  String w[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
  return w[weekday()-1];
  }

long getCST() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  //132.163.4.101, 132.163.4.102, 132.163.4.103
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

新加上的程式碼以藍色表示, 這裡要注意的是 1602 可以顯示如攝氏度數符號 ° 等等的特殊符號, 其內碼 (pattern code) 可參考 1602 內的 HD44780 晶片規格書之 Page 17 :

# https://www.sparkfun.com/datasheets/LCD/HD44780.pdf

可知攝氏度數符號之內碼為 B11011111 或 0xDF, 化成十進制即 223 也. 但在使用 lcd.print() 輸出前必須使用 char() 予以強制轉型為字元. 關於 1602 的特殊符號也可參考  "Arduino Cookbook 錦囊妙計" 這本書的 11.5 節. 不過我照其範例直接輸出 B11011111 或 0xDF 或 223 都無法顯示攝氏度數符號, 後來找到下列資訊才知道要用 char() 轉型 :

# http://forum.arduino.cc/index.php?topic=19002.0


這樣與網路同步的日期時間以及溫濕度資訊都完整呈現於 1602 顯示器, 這部分的實驗到這裡總算告一段落啦! Time 與 TimeAlarms 函式庫還有很多好用函式, 以後有用到或有空再來測試囉.

2016-08-05 :

NTP 回應訊息的擷取方式已改版, 參見 :

# 利用 NTP 伺服器來同步 Arduino 系統時鐘 (三)



Arduino 與樹苺派實驗常用 IC

今天整理電子實驗可能會用到的 IC, 查詢並紀錄露天賣場供貨來源, 下次採購時盡量一次買齊, 免得要用到時還要個別網購, 郵資很貴的哩. 下面這兩顆 IC 是我在 "Arduino 互動設計專題實戰" 這本書看到的. 其中 74HC595 與 LM393 在第 14 章有專章介紹; 而 MAX7219 在 12.21.1 節介紹 8*8 LED 點矩陣時用到.

1. 74HC595 : 8 位元移位暫存器

此顆 IC 可用來擴展 Arduino 的數位輸出入埠, 只需用 Arduino 三個 IO 腳即可控制 74HC595 的 8 個資料輸出腳, 可以節省 Arduino 的腳位, 例如用來推動七節 LED 顯示器. 以 UNO 或 NANO 來說, 它們只有 D0~D13 共 14 個 DIO 埠, 其中 D0 被用做硬體串列埠的 TX, D1 被用做 RX, 所以實際上只剩下 12 個可用.

# 【DIY_LAB#172A】74HC595/SN74HC595D(SOP16) 8bit串入,串或並出位移記錄器(現貨) $4
# 【DIY_LAB#172】74HC595/SN74HC595N(DIP16) 8bit串入,串或並出位移記錄器(現貨) $4
# ►130◄74HC595 直插 74HC595 8位元串列輸入/輸出/並行輸出暫存器 DIP-16 LED驅動使用 $4

2. LM393 : 類比電壓比較器

此 IC 用來比較兩個類比電壓, 若 +IN > -IN, 就輸出 HIGH, 否則輸出 LOW. 可用來比較感測器輸出是否超過既定值. 此 IC 有 DIP 與 SMD 兩種包裝, 裡面有 IN1 與 IN2 兩個比較器.

# 【DIY_LAB#168】LM393N/LM393P (DIP8) Low Offset 2組低功耗電壓比較器(現貨) $2
# 【DIY_LAB#54】LM393 LMT393 (SOP-8) Low Offset 2組電壓比較器(現貨) #2
# 柏毅電子$UTC LM393L UTC電子零件 $4

3. MAX7219/7221

此款 MAX72XX 系列晶片為使用 SPI 協定的移位暫存器, 是專為共陰極七段顯示器而設計, 用在控制多個七段顯示器, 但想減少所使用的 Arduino 接腳時. 參考 "Arduino Cookbook 錦囊妙計" 7.12 與 7.13 節.


# [X-LAN] MAX7219/MAX7219CNG 共陰8位數LED顯示驅動IC $22
# ►430◄MAX7219 顯示器驅動器 LED 驅動器 8位 DIP-24 IC $24
# 【DIY_LAB#671A】MAX7219CWG(SOP24)共陰8位數LED顯示驅動IC Arduino等適用(現貨) $22
# 【DIY_LAB#671】MAX7219 (DIP24) 共陰8位數LED顯示驅動IC Arduino等適用(現貨) $24
# MAX7221CNG MAX7221ENG DIP/SOP (2個一拍) $209


4. MCP23017 : 16 埠數位輸出埠擴增器

這顆 IC 是我從 "Arduino 自造指南" 的專案 55 看到的, 是一種透過 I2C 控制的埠擴增 IC, 可讓 Arduino 增加 16 個 GPIO 埠, 其價格比 74HC595 貴, 可參考下面第一個 Proteus 的詳細說明 :

# Microchip MCP23017-(I2C協議) 16通道IO擴充IC(可用於單晶片、Arduino、樹莓派..等) $96
# 《iCshop1》MCP23017-E/SP●368010101406● $56
# 《iCshop1》MCP23017T-E/SS●368010101405● $59

5. TLC5940 : PWM 埠擴充晶片

使用 PWM 來模擬類比輸出時會用到 Arduino 的 D3, D5~D6, D9~D11 這六個 PWM 接腳, 例如控制 LED 光立方燈, PWM 腳可能不夠, 這時就需要用到 TLC5940 來減少所需之 PWM 腳數. 此晶片只需 5 個數位輸出腳 (其中三個為 PWM) 即可控制 16 個 LED. 參考 "Arduino Cookbook 錦囊妙計" 7.14.

# 【DIY_LAB#1086】TLC5940NT(DIP28) 16通道LED驅動器 具點校正與灰度PWM控制光立方_原裝 $80
# 《icshopping_com》TLC5940NT【3680101008197】 $252
# TLC5940NT(TI)背光LED驅動器LED DRIVER PWM CONTROL 28-DIP原裝現貨可當天出貨 $160
# TLC5940PWP(TI)LED照明驅動裝置 4096 Step 28-SSOP全新原裝現貨可當天出貨 $280

6. 24LC128 : EEPROM

Arduino 的 ATMEGA328P 微控器內建 512 Bytes 的 ROM, 不夠用時便需要外加 EEPROM 晶片. 這顆 24LC128 具有 128KB 的容量, 採用 I2C 協定與 Arduino 的兩個類比針腳 (A0~A5) 相接. 參考 "Arduino Cookbook 錦囊妙計" 13.4.

# 24LC128I--已到貨 $10
# 【尚敏LCD零件專賣店】全新原裝 24LC128I 24LC1281 24LC128 #24
# 《icshopping_com》MC24LC128-I/P【3680101004182】$26
# 《ichome3689》MC24LC128-I/P【3680101004182】 $26


補記 : 向露天 Q 小奇採購零件一批

前陣子 blogger 似乎怪怪的, 編輯文章之後會出現另一筆同樣內容的文章, 看起來好像是把 edit 當作 add 在處理, 所以只好把重複的文章刪除, 可能因為這樣把 6 月 28 日向 Q 小奇採購零件的紀錄刪除了. 這樣會造成我無法追蹤曾經買過甚麼零件, 所以在此補記.

品名
數量
單價
小計
2
12
24
23
2
46
2
20
40
4
35
140
1
29
29


也有可能是 Chrome 的問題, 因為不是每一台電腦都會這樣. 總之以後出現類似情況時, 不要手太賤亂刪紀錄.


2016年7月17日 星期日

2016 年第 27 周記事

颱風過後通常都是酷熱的天氣, 據我不專業的氣象分析, 應該是低氣壓走後換高氣壓來, 這樣太陽輻射下來的熱更不容易消散, 所以會非常悶熱. 這幾天熱到不行, 這星球真的不同了, 以前小時候沒這麼熱吧?

昨天突發奇想邀剛好完成東部自行車之行的阿龍表弟去騎阿公店水庫路線, 在這酷熱的天氣真是瘋了. 還好早上下了場小雨, 不時飄來的烏雲當了擋箭牌, 如果一整天都是像响午之後的艷陽, 我看會中暑. 表弟說下回要挑戰南迴公路, 但這天氣 ... 是不是冬天來騎會比較舒服?

因為二哥會考順利考上鳳中, 要回鄉下的文昌帝君廟還願, 所以本周是二哥與菁菁與我回鄉下. 早上三人一起去市集買菜以及拜拜用的水果, 回程經過帝君廟去還願, 發現今日還願祈福人數不少, 過了 11 點才回家. 結果小舅他們已到了, 小舅媽 15 日自美返國, 買了一件 Polo 衫給我, 一罐維骨力給爸, 還有楓糖巧克力給小狐狸們. 這巧克力 ... 是我吃過最甜的巧克力! 難道楓糖甜度比較高嗎? 下午燜完苦瓜, 得空來組裝上周記來的鑽床, 只有底座, 鑽台, 還有上面的馬達與鑽頭三大部件, 很容易組裝. 但是最後要固定鑽頭時才發現似乎少了一個固定栓, 明天再跟廠家詢問看看.

本周處理央廣問題時需要台北方面協助, 電話很巧轉到我以前剛進公司時候的老長官李班長, 寒暄之餘探問了另一位老長官郭班長近況, 我問他是否已退休, 李班長說老郭退休好多年了, 也走了好多年了. 我一聽, 蛤? 是那個走嗎? 他說是啊, 退休沒多久身體就出問題了. 記得郭班長經常兩肘撐在茶水間的窗台上, 望著窗外猛抽他的黑煙斗, 一派優閒紳士的樣子. 我也曾當過他的跟班到市區做工程, 他對我們菜鳥還蠻照顧的, 不曾頤指氣使. 我離開台北之後就鮮少跟老單位聯絡, 李班長說那時的老同事都已四散各處, 人事全非了. 聽他這麼說, 念舊的我心裡頓時非常的 blue.

2016-07-18 補充 :

今天問了尚椿五金, 原來只要將鑽頭的爪收起來, 然後套上轉軸後, 用一個木槌往上敲進去就會卡住了, 師傅特別強調一定要將爪收進去, 否則容易將爪弄壞. 只能下周末回去再試試看了.

# 日本系統 ASAHI ZJ4113 8吋 桌上型鑽床 五段調速/四分夾頭/1/3hp強力馬達 鐵工/木工/鑽孔 非DP8


2016年7月16日 星期六

遊阿公店水庫

昨天阿龍表弟 line 我說他的花東自行車之旅已結束, 人在台東. 我問要不要來高雄, 請他吃個飯. 他說好啊, 順便逛逛高雄附近. 他說的逛逛是指騎自行車啦.

我想難得來高雄, 我又沒去過佛陀紀念館, 就邀他乾脆騎到那裏吃午飯, 聽同事說紀念館樓上有個素食餐廳, 挺好吃的. 晚上 11 點半到高鐵接他去舅媽家, 敲定今日七點出發. 結果回來 12 點半又寫了一下程式, 一點多才睡, 怎麼可能七點出發, 開玩笑.

今早 08:06 從家裡出發, 沿著民族路騎到大社舅媽家已經 08:50 了, 光這一段應該有 12 公里吧. 小舅媽建議我們騎到阿公店水庫比較近, 有環湖自行車道, 很平很好騎, 我想也不錯, 就改計畫到阿公店了. 哪知我們看錯地圖, 走到菜寮路, 一連串上坡, 一坡未平一坡又起, 這時就不免埋怨起小舅媽, 她自己到底有沒有騎過這條路啊! 甚麼很平很好騎!

一路上我看路牌, 不僅經過燕巢, 還經過田寮, 再騎一段就快到橋頭岡山了. 繞了一大圈終於回到燕巢, 這時已經 12:30 了. 騎回楠梓的路上剛好看到好大一間剉冰店, 兩人各吃了一大碗冰, 看看時間似乎要搭 13:55 的高鐵有點趕, 就說不給我請吃飯了, 騎到楠梓車站他就上火車到新左營. 我則還要頂著豔陽走高楠公路慢慢騎回家, 我太久沒操, 我騎到腳都快抽筋了.

中午一個人在楠梓池上便當吃午飯時, 表弟傳 LINE 說 : 表哥, 我們騎錯了, 不該走菜寮路, 應該走旁邊那個自行車步道, 你查 Google 地圖就知道了. 卡好, 難怪繞了一大圈. 舅媽, 是我們錯怪你了.

今天一言以蔽之, 就是二傻騎自行車遊阿公店水庫.


利用 NTP 伺服器來同步 Arduino 系統時鐘 (一)

上回在做 IOT 模組的 NTP 對時實驗時, 找到了 Arduino 新版 Time 函式庫的說明文件, 那時以為這個函式庫必須搭配 RTC 模組才能使用就沒仔細看. 週三晚上去河堤國小附近把姊姊忘記騎回來的 "小金" (我的小小摺疊腳踏車) 找回來後, 就大略地閱讀了這個 Time 函式庫的說明 :

# Arduino Time library

發現原來此函式庫不一定要用到 RTC, 它只是提供一堆跟時間有關的函式方便我們處理日期時間而已, 我們可以透過 setTime() 函式讓 Arduino 與外部時鐘同步, 而 RTC 只不過是同步來源的一種而已, 也可以透過網路與 NTP 伺服器同步 :

"The Time library adds timekeeping functionality to Arduino with or without external timekeeping hardware."

可從 Github 下載 Time 函式庫 :

# https://github.com/PaulStoffregen/Time

把下載下來的 Time-master.zip 檔解壓縮後放到 Arduino IDE 的安裝目錄的 libraries 資料夾下面, 重開 IDE 就會抓到此函式庫了. 在 Arduino Time library 底下有一個範例程式, 我把範例程式整理並加註中文說明如下 :

測試 1 :

#include <Time.h>

#define TIME_MSG_LEN  11   //時間字串長度含標頭 T 為 11 字元 (T+10位數之 Unix 時戳)
#define TIME_HEADER  'T'   //時間同步訊息標頭
#define TIME_REQUEST  7    //ASCII bell character requests a time sync message

//時間字串格式 T1262347200  //noon Jan 1 2010

void setup()  {
  Serial.begin(9600);
  }

void loop() {  
  if (Serial.available()) { //串列埠收到訊息
    processSyncMessage(); //處理時間同步字串
    }
  if (timeStatus() == timeNotSet) { //時間尚未同步
    Serial.println("waiting for sync message");
    }
  else { //時間已同步, 顯示時間
    digitalClockDisplay();
    }
  delay(1000);
  }

void digitalClockDisplay() { //顯示時間
  // digital clock display of the time
  Serial.print(hour());   //時
  printDigits(minute()); //分
  printDigits(second()); //秒
  Serial.print(" ");
  Serial.print(day());    //日
  Serial.print(" ");
  Serial.print(month());  //月
  Serial.print(" ");
  Serial.print(year());   //年
  Serial.println();
  }

void printDigits(int digits) { //顯示數值 & 位數調整
  // utility function for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if (digits < 10) {Serial.print('0');}
  Serial.print(digits);
  }

void processSyncMessage() { //處理同步時間字串
  //若自串列埠收到同步時間訊息, 更新時間並回傳 true
  while (Serial.available() >=  TIME_MSG_LEN ) {  //收到至少 11 字元的同步訊息
    char c=Serial.read();
    Serial.print(c);
    if (c==TIME_HEADER ) { //收到同步訊息標頭 'T'    
      time_t pctime=0; //定義時間變數初值 (整數)
      for (int i=0; i<TIME_MSG_LEN-1; i++) { //讀取 Unix 時戳字串
        c=Serial.read();        
        if (c >= '0' && c <= '9') { //必須是 0~9 數字
          pctime=(10 * pctime) + (c - '0') ; //將數字字元轉成整數
          }
        }
      setTime(pctime); //將 Arduino 內部時鐘與自串列埠收到的時間訊息同步
      }
    }
  }

上傳執行後, 序列埠監控視窗會不斷出現 "waiting for sync message", 輸入 T1262347200 按傳送就會將 Arduino 內部時鐘與此時間訊息同步, 序列埠監控視窗顯示如下 :

waiting for sync message
waiting for sync message
waiting for sync message
T12:00:00 1 1 2010
12:00:01 1 1 2010
12:00:02 1 1 2010
12:00:03 1 1 2010
12:00:04 1 1 2010
12:00:05 1 1 2010
12:00:06 1 1 2010

注意第一個時間訊息前的 T 來自 if 判斷前的 Serial.print(c).

接下來我想修改上回 NTP 對時程式來使 Arduino 內部時鐘與其同步, 參考 :

# 以 Arduino + ESP8266 物聯網模組重作 NTP 實驗 (三)

我參考上面範例以及之前我做的 NTP 實驗, 希望從 NTP 取得 GMT 的 Unix 時間後, 加上 8 小時 (28800 秒) 得到台灣時間的 Unix 秒數, 再傳給 Time 函式庫的 setTime() 函數來設定 Arduino 的系統時鐘, 這樣就能透過此函式庫來取得完整的日期與時間訊息了, 如下面測試 2 所示 :

測試 2 :

#include <SoftwareSerial.h>
#define DEBUG true
#include <Time.h>

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP
int i=0; //sync cycle counter

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  setTime(getTime());
  }

void loop() {
  showDateTime();
  ++i;
  if (i >= 60) {  //1 分鐘同步週期到了
    setTime(getTime());  //設定 Arduino 系統時鐘
    i=0;
    }
  else {delay(1000);}  //一秒跳一次
  }

void showDateTime() { //輸出如 2016-07-15 19:56:12 格式的日期時間
  Serial.print(year());  
  Serial.print("-");
  byte M=month();
  if (M < 10) {Serial.print('0');}
  Serial.print(M);
  Serial.print("-");  
  byte d=day();
  if (d < 10) {Serial.print('0');}
  Serial.print(d);  
  Serial.print(" ");
  byte h=hour();  
  if (h < 10) {Serial.print('0');}
  Serial.print(h);
  Serial.print(":");
  byte m=minute();
  if (m < 10) {Serial.print('0');}
  Serial.print(m);
  Serial.print(":");  
  byte s=second();
  if (s < 10) {Serial.print('0');}
  Serial.println(s);
  }

long getTime() {  //從 NTP 伺服器取得 Unix 時間秒數
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;  //台灣時間為 GMT+8 小時, 即 28800 秒
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

在 setup() 函式中, 我們先呼叫自訂的 getTime() 函數從 NTP 伺服器取得台灣時區的 Unix 秒數, 再將其傳入 Time 函式庫的 setTime() 函數來設定 Arduino 內部時鐘. 而在 loop() 迴圈裡, 先呼叫自訂的 showDateTime() 函數來顯示目前的日期時間, 然後增量同步計數器 i, 若到達 60 次時就呼叫 getTime() 來與 NTP 伺服器對時, 並且將計數器歸零.

程式上傳後擷取串列埠監控視窗訊息如下 :

AT+RST


OK
bB�鑭b禔 ��"兀\�侒��餾�
[System Ready, Vendor:www.ai-thinker.com]
AT+GMR

0018000902

OK
AT+CIFSR

192.168.2.108

OK
Sending request to NTP server ...
AT+CIPSTART="UDP","91.226.136.136",123


OK
AT+CIPSEND=48

>
NTP server answered :
E3 00 06 EC 00 00 00 00 00 00
00 00 31 4E 31 34 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 0D 0A
53 45 4E 44 20 4F 4B 0D 0A !!!0D
0A 2B 49 50 44 2C 34 38 3A 24
02 06 ED 00 00 00 22 00 00 02
44 6B DC 0B 87 DB 33 43 02 94
20 CF 9D 00 00 00 00 00 00 00
00 DB 33 44 59 E4 D7 13 48 DB
33 44 59 E4 D7 9F D7 0D 0A 4F
4B 0D

123 bytes received
NTP time stamp packets (byte 101~104)=DB 33 44 59
NTP time stamp (seconds since 1900-01-01)=3677570137
Unix time stamp (seconds since 1970-01-01)=1468581337
AT+CIPCLOSE


OK
Unlink
2016-07-15 19:15:37
2016-07-15 19:15:38
2016-07-15 19:15:39
2016-07-15 19:15:40
2016-07-15 19:15:41
2016-07-15 19:15:42
2016-07-15 19:15:43
... (每秒跳一次)

2016-07-15 19:16:29
2016-07-15 19:16:30
2016-07-15 19:16:31
2016-07-15 19:16:32
2016-07-15 19:16:33
2016-07-15 19:16:34
2016-07-15 19:16:35
2016-07-15 19:16:36
AT+CIPSTART="UDP","91.226.136.136",123


OK
AT+CIPSEND=48

>
NTP server answered :
E3 00 06 EC 00 00 00 00 00 00
00 00 31 4E 31 34 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 0D 0A
53 45 4E 44 20 4F 4B 0D 0A !!0D
0A 2B 49 50 44 2C 34 38 3A 24
02 06 ED 00 00 00 22 00 00 02
86 6B DC 0B 87 DB 33 43 02 94
20 CF 9D 00 00 00 00 00 00 00
00 DB 33 44 9D 64 A8 39 88 DB
33 44 9D 64 B2 E4 33 0D 0A 4F
4B 0D 0A

124 bytes received
NTP time stamp packets (byte 101~104)=DB 33 44 9D
NTP time stamp (seconds since 1900-01-01)=3677570205
Unix time stamp (seconds since 1970-01-01)=1468581405
AT+CIPCLOSE


OK
Unlink
2016-07-15 19:16:45
2016-07-15 19:16:46
2016-07-15 19:16:47
2016-07-15 19:16:48
2016-07-15 19:16:49
2016-07-15 19:16:50
...

可見每次同步大約花掉 8 秒時間.

上面測試二使用一個同步計數器變數 i 來計算是否該與 NTP 伺服器同步了, 其實這可以用 TimeAlarms 函式庫來做, 同樣從 Github 的 Clone or download 下載  :

# https://github.com/PaulStoffregen/TimeAlarms

解壓縮 TimeAlarms-master.zip 至 Arduino IDE 安裝目錄的 libraries 下即可. 程式如下 :

測試 3 :

#include <SoftwareSerial.h>
#define DEBUG true
#include <Time.h>
#include <TimeAlarms.h>

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  sync_clock();
  Alarm.timerRepeat(60, sync_clock);  //設定每 60 秒觸發執行 sync_clock()
  }

void loop() {
  showDateTime();
  Alarm.delay(1000);   //每一秒檢查一下觸發條件
  }

void sync_clock() {
  setTime(getTime());
  }

void showDateTime() {
  Serial.print(year());  
  Serial.print("-");
  byte M=month();
  if (M < 10) {Serial.print('0');}
  Serial.print(M);
  Serial.print("-");  
  byte d=day();
  if (d < 10) {Serial.print('0');}
  Serial.print(d);  
  Serial.print(" ");
  byte h=hour();  
  if (h < 10) {Serial.print('0');}
  Serial.print(h);
  Serial.print(":");
  byte m=minute();
  if (m < 10) {Serial.print('0');}
  Serial.print(m);
  Serial.print(":");  
  byte s=second();
  if (s < 10) {Serial.print('0');}
  Serial.println(s);
  }

long getTime() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

這裡我們新增了一個無傳回值的函數 sync_clock() 用來使 Arduino 與 NTP 時間同步, 然後於 setup() 中先初始同步一次後, 再用 Alarm.timeRepeat() 函式設定每 60 秒觸發 sync_clock() 一次. 而在 loop() 中則改用 Alarm.delay(1000) 函數來延遲一秒, 而非 delay(1000), 因為這樣才會每一秒去檢查事件狀態, 也就是 Alarm.timerRepeat() 所設定的 60 秒觸發是否已滿足.

但是觀察序列埠監控視窗擷取訊息發現, 剛開始似乎一切都正常, 但是大約十次之後就不再觸發了, why?  不知何故, 今天重新執行又一切正常.

下面測試 4 是將測試 3 無傳回值的 showDateTime() 改寫為傳回日期時間字串的 getDateTime(), 功能完全一樣 :

測試 4 :

#include <SoftwareSerial.h>
#define DEBUG true
#include <Time.h>
#include <TimeAlarms.h>

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  sync_clock();
  Alarm.timerRepeat(60, sync_clock);
  }

void loop() {
  Serial.println(getDateTime());
  Alarm.delay(1000);
  }

void sync_clock() {
  setTime(getTime());
  }

String getDateTime() {  //傳回日期時間
  String dt=(String)year() + "-";
  byte M=month();
  if (M < 10) {dt.concat('0');}
  dt.concat(M);
  dt.concat('-');
  byte d=day();
  if (d < 10) {dt.concat('0');}
  dt.concat(d);
  dt.concat(' ');
  byte h=hour();
  if (h < 10) {dt.concat('0');}
  dt.concat(h);
  dt.concat(':');
  byte m=minute();
  if (m < 10) {dt.concat('0');}
  dt.concat(m);
  dt.concat(':');
  byte s=second();
  if (s < 10) {dt.concat('0');}
  dt.concat(s);
  return dt;  //傳回格式如 2016-07-16 16:09:23 的日期時間字串
  }

long getTime() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

在測試 4 的 getDateTime() 函數中使用了 + 與字串的 concat() 兩種方式來串接日期字串. 此程式編譯後記憶體耗用結果 :

草稿碼使用了 10,838 bytes (35%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 866 bytes (42%) 的動態記憶體,剩餘 1,182 bytes 供局部變數。最大值為 2,048 bytes 。

最後一個測試我想把這個每分鐘與 NTP 同步的系統時鐘顯示在 1602 LCD 顯示器上, 參考之前的測試文章 :

# 以 Arduino + ESP8266 物聯網模組重作 NTP 實驗 (三)

在此測試中, LCD 上排顯示 IP, 下排顯示如 23:12:59 的時間, 因為 1602 顧名思義就是只能顯示 16*2 共 32 個字元, 而測試 4 中 getDateTime() 所傳回的是如 2016-07-16 16:57:02 格式的字串, 共 19 個字元, 無法於一排中全部顯示, 因此在下列測試 5 中, 我捨棄顯示 ip, 將傳回的日期時間字串以空格拆分後, 日期顯示在上排, 時間顯示在下排, 程式如下 :

測試 5 :

#include <SoftwareSerial.h>
#define DEBUG true
#include <Time.h>
#include <TimeAlarms.h>
#include <LiquidCrystal.h>
#define RS 2
#define E 3
#define D4 10
#define D5 11
#define D6 12
#define D7 13

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP
LiquidCrystal lcd(RS,E,D4,D5,D6,D7);  //create LCD object

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  sync_clock();
  Alarm.timerRepeat(60, sync_clock);
  lcd.begin(16,2); //define 2*16 LCD
  lcd.clear(); //clear screen
  }

void loop() {
  String dt=getDateTime();
  Serial.println(dt);
  String d=dt.substring(0,dt.indexOf(" "));  //以空格拆分日期與時間
  String t=dt.substring(dt.indexOf(" ") + 1);  //空格後為時間
  lcd.setCursor(0,0); //move to (x,y)
  lcd.print(d); //print date  
  lcd.setCursor(0,1); //move to (x,y)
  lcd.print(t); //print time    
  Alarm.delay(1000);
  }

void sync_clock() {
  setTime(getTime());
  }

String getDateTime() {
  String dt=(String)year() + "-";
  byte M=month();
  if (M < 10) {dt.concat('0');}
  dt.concat(M);
  dt.concat('-');
  byte d=day();
  if (d < 10) {dt.concat('0');}
  dt.concat(d);
  dt.concat(' ');
  byte h=hour();
  if (h < 10) {dt.concat('0');}
  dt.concat(h);
  dt.concat(':');
  byte m=minute();
  if (m < 10) {dt.concat('0');}
  dt.concat(m);
  dt.concat(':');
  byte s=second();
  if (s < 10) {dt.concat('0');}
  dt.concat(s);
  return dt;
  }

long getTime() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

程式上傳後運作正常, 秒數會每一秒跳一下, 只有在每分鐘要同步時會暫停跳動約 9~10 秒 :



請注意當時間跑到 16:44:03 時會停止約 9 秒, 然後直接跳到 16:44:12, 這 9 秒就是 NTP 同步所花的時間.