2017年5月20日 星期六

MicroPython on ESP8266 (七) : 時間日期測試

搞定 MicroPython 的檔案系統問題後, 下一步本想接著測試 GPIO, 但是突然想到在測候站或 PIR 移動偵測等應用中需紀錄時間, 不知 MicroPython 對日期時間模組的支援是如何?  馬上來測試一番 :

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>> import time                                     #有支援 time 模組
>>> from datetime import date            #未支援 datetime 模組
Traceback (most recent call last):
  File "ImportError: no module named 'datetime'
>>> import calendar                              #未支援 calendar 模組
Traceback (most recent call last):
  File "ImportError: no module named 'calendar'

所以 MicroPython 在 Python 內建模組方面僅支援 time 模組而已. 

這個 time 模組裡面有一個 time() 函數, 它會傳回目前 UTC/GMT 時間的 epoch 秒數. 所謂 epoch 是指從 Unix 開始運作的時間 1970 年 1 月 1 日起算到現在的秒數, 但是在 MicroPython 則是從 2000 年 1 月 1 日起算. Epoch 是在不同系統之間交換日期時間的最佳媒介.

我使用 1MB 的 ESP-01 模組測試, 發現在從未設定過時間的情形下, time.time() 會傳回開機後的秒數, 例如 :

>>> import time
>>> time.time()     #傳回開機後秒數
4561
>>> time.time()
4564
>>> time.time()
4589
>>> time.time()

4729

4500 多秒大概是開機後 1 小時又 15 分鐘左右. 

time 模組有一個 ctime() 函數可以將目前的 epoch 時間轉成時間字串, 例如在 CPython :

C:\Users\user>python
Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import time
>>> now=time.time()    #傳回 UTC 的 epoch 秒數
>>> now
1495270100.4163206
>>> time.ctime(now)     #將 epoch 秒數轉成時間字串
'Sat May 19 08:48:20 2017'            #轉成 UTC 時間字串  

但是 MicroPython 未實作 ctime() 函數 :

>>> import time
>>> time.ctime(time.time())       #MicroPython 沒有實作 ctime()          
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'ctime'

不過 MicroPython 的 time 模組倒是實作了 localtime() 函數, 它會傳回從 2000 年 1 月 1 日 0 時 0 分 0 秒起算的時間字串, 例如 :

>>> import time
>>> now=time.time()
>>> now                                #自 2000/1/1 起之秒數
23093
>>> time.localtime(now)     #將 epoch 轉成日期時間字串
(2000, 1, 1, 6, 24, 53, 5, 1)


其傳回值分別表示 (年, 月, 日, 時, 分, 秒, 星期, 年日), 倒數第二個的星期值為 0~6, 其中 0 表示星期一, 6 表示星期天; 而年日表示這是一年中的第幾天, 參考 :

https://docs.micropython.org/en/latest/pyboard/library/utime.html#utime.localtime

要如何才能讓 ESP8266 顯示正確時間呢? 如果使用 Pyboard 等板子可以採用 RTC 來提供目前時間, 但對於只拉出兩個 I/O 埠的 ESP-01 就太麻煩了, 最方便的方法是利用 ESP8266 本身自有的 WiFi 連網功能從 NTP 伺服器取得同步時間, 而且 MicroPython 有實作 ntptime 模組, 其 settime() 函數會以從 NTP 伺服器取得的 UTC 時間去設定 ESP8266 內的 RTC :

參考 :

# Setting RTC From Internet?

但是存取 NTP 伺服器須先讓 ESP8266 連上網路, 參考本系列測試中的 WiFi 測試 :

MicroPython on ESP8266 (二) : 數值型別測試
MicroPython on ESP8266 (三) : 序列型別測試
MicroPython on ESP8266 (四) : 字典與集合型別測試
MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
# MicroPython on ESP8266 (六) : 檔案系統測試

MicroPython 文件參考 :

MicroPython tutorial for ESP8266  (官方教學)
http://docs.micropython.org/en/latest/micropython-esp8266.pdf
http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket
https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)

>>> import network                                                       #匯入 NETWORK 模組
>>> sta_if = network.WLAN(network.STA_IF)           #取得 STA 介面物件
>>> sta_if.connect('EDIMAX-tony', '1234567890')      #連線 AP
>>> sta_if.ifconfig()                                                       #檢視 STA 設定
('192.168.2.122', '255.255.255.0', '192.168.2.2', '192.168.2.2')     #取得 IP 192.168.2.122

取得 IP 表示已連線至 AP, 接下來便可以透過 Internet 連線從 NTP 伺服器取得 UTC 時間了. MicroPython 有實作了 ntptime 模組, 其中的 settime() 方法可用來設定 ESP8266 內部時鐘, 然後再用 time 模組的 localtime() 方法即可取得目前的 UTC 時間, 例如 :

>>> from ntptime import settime         #從 ntptime 模組匯入 settime()
>>> settime()                                           #以自 NTP 伺服器取得之 UTC 時間設定 RTC
(2017, 5, 20, 7, 19, 40, 5, 140)                              
>>> time.localtime()                                #顯示目前之 UTC 時間
(2017, 5, 20, 7, 21, 19, 5, 140)

但是由於 MicroPython 未實作 locale 模組, 因此無法藉此設定時區來取得台灣時間 :

>>> import locale  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: no module named 'locale'

如果要取得本地而非 UTC 時間, 必須自行換算, 例如台灣時間為 UTC+8, 我們可以將 time.localtime() 傳回的 tuple 傳給 time.mktime(), 它會將此 tuple 轉成 Epoch 秒數, 將其加上 28800 秒 (= 8 小時) 便是台灣時間的 Epoch 秒數, 最後再傳給  time.localtime() 即可得到台灣時間的 tuple, 例如 :

>>> time.mktime(time.localtime())     #將 RTC 時間轉成 UTC 之 Epoch
548580535
>>>
>>> time.localtime(time.mktime(time.localtime())+28800)    #UTC 加 8 小時
(2017, 5, 20, 15, 38, 26, 5, 140)                                                               #為台灣時間

這樣要製作我慣用的 YYYY-MM-DD HH:mm:SS 時間格式字串就很容易啦, 例如 :

測試 1 : 從 NTP 伺服器取得 UTC 時間並轉為台灣時間

import time
from ntptime import settime
settime()
utc_epoch=time.mktime(time.localtime())
Y,M,D,H,m,S,ms,W=time.localtime(utc_epoch + 28800)
t=str(Y) + '-'
if M<10:
    t += '0' + str(M)
else:
    t += str(M)
t += '-'
if D<10:
    t += '0' + str(D)
else:
    t += str(D)
t += ' '
if H<10:
    t += '0' + str(H)
else:
    t += str(H)
t += ':'
if m<10:
    t += '0' + str(m)
else:
    t += str(m)
t += ':'
if S<10:
    t += '0' + str(S)
else:
    t += str(S)
print(t)

將上面程式存成 t.py 用 ampy 上傳 ESP8266 後執行結果如下 :

D:\test>ampy --port COM4 put t.py     #上傳 RSP8266

D:\test>ampy --port COM4 run t.py     #執行程式
(2017, 5, 20, 15, 28, 3, 5, 140)
2017-05-20 23:28:03

注意, 下面這篇文件提到, ESP8266 內建 RTC 每 7 個小時 45 分會溢位, 因此最好 7 個小時內要呼叫 time() 或 localtime() 函數一次, 這樣 MicroPython 才能處理溢位問題, 參考 :

http://docs.micropython.org/en/v1.8.7/esp8266/esp8266/general.html#real-time-clock

"Due to limitations of the ESP8266 chip the internal real-time clock (RTC) will overflow every 7:45h. If a long-term working RTC time is required then time() or localtime() must be called at least once within 7 hours. MicroPython will then handle the overflow."

參考 :  

MicroPython Libraries
https://docs.micropython.org/en/latest/pyboard/library/utime.html
Timezone support for "time" module #2130
Date/Time management
15.3. time — Time access and conversions (Python 標準函式庫)
MicroPython标准库函数utime
NTP update micropython time


2017-07-05 補充 :

上面的程式可以寫個 fill_zero() 函數來簡化 :

測試 2 : 從 NTP 伺服器取得 UTC 時間並轉為台灣時間-簡化版

def fill_zero(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)
import time
import ntptime
ntptime.settime()
while True:
    utc_epoch=time.mktime(time.localtime())
    Y,M,D,H,m,S,ms,W=time.localtime(utc_epoch + 28800)
    t='%s-%s-%s %s:%s:%s' % (str(Y),fill_zero(M),fill_zero(D),\
                             fill_zero(H),fill_zero(m),fill_zero(S))
    print(t)
    time.sleep(1)

此程式使用 time.sleep() 每秒讀取 ESP8266 內建時鐘後轉成台灣時間, 然後使用字串格式化技巧產生時間字串, REPL 介面輸出如下 :

2017-07-05 20:03:37
2017-07-05 20:03:38
2017-07-05 20:03:39
2017-07-05 20:03:40
2017-07-05 20:03:41
2017-07-05 20:03:42
2017-07-05 20:03:43
2017-07-05 20:03:44
2017-07-05 20:03:45
2017-07-05 20:03:46
2017-07-05 20:03:47
2017-07-05 20:03:48
2017-07-05 20:03:49


2017-08-29 補充 :

由於 MicroPython 尚未支援 datetime, 所以無法使用 datetime.strftime() 將時戳依照指定格式轉成如 "2017-08-29 12:34:21" 的字串. 如果要將特定日期時間字串轉成其他時區, 必須自行進行字串處理, 將其轉成 8 元素的串列或元組傳給 time.maketime() 轉成時戳才能進行整數計算, 最後將時戳傳給 time.localtime() 轉回串列.

例如 "2017-08-29 12:34:12" 這個字串用空格, '-' 與 ':' 以 split() 拆開後只能得到 6 個元素, 必須在拆開前在後面補上 '0:0' 變成 "2017-08-29 12:34:12:0:0" 再拆才不會出錯. 實際範例參考下列文章的測試 4 :

MicroPython on ESP8266 (二十) : 從 ThingSpeak 讀取資料

參考 :

[Python] 時間格式轉換(strtime & strftime)
Converting UTC in utime.localtime giving wrong date
micropython/micropython-lib  (支援 strftime)
How to convert python timestamp string to epoch?

4 則留言 :

Yi-Siang Ciou 提到...

老師您好 又來請教非常基本的問題了
pin.value(0)的問題解決後
想隨便寫個for之類的小程式在8266上面跑
但不管如何都會出現
Traceback (most recent call last):
File "", line 1
SyntaxError: invalid syntax

而同樣的code如
print ("Hello, Python")
寫成hello.py 在Linux上執行都是正常的
請問這是路徑問題嗎?
我用os.listdir()是能直接看到的
我比較熟悉Linux上的指令 不太了解esp8266

Tony Huang 提到...

您的程式碼長怎樣呢?

Yi-Siang Ciou 提到...

老師您好 我截圖比較容易閱讀
http://imgur.com/a/OdP9D
像這樣的python檔我放在樹莓派上都能正常運作
但用Webrepl放到esp上執行就是無法

Tony Huang 提到...

REPL 介面跟 Python IDLE 介面一樣直接輸入 Python 指令, 而 python hi.py 並非 Python 指令, 而是作業系統命令, REPL 當然不認得. 給 MicroPython 跑的 Python 程式要存成 main.py 利用 ampy 等工具上傳到 ESP8266 的 MicroPython 檔案系統跟目錄下, 在 REPL 按 Ctrl+D 才會執行, 參考 :

http://yhhuang1966.blogspot.tw/2017/05/micropython-on-esp8266_18.html