2019年7月9日 星期二

MicroPython on ESP32 學習筆記 (四) : RTC 與日期時間

MircoPython 的日期時間測試可參考之前在 ESP8266 上的測試記錄 :

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

在 v.11 版韌體與 ESP32 上其實跟 ESP8266 一樣, 即 CPython 上的 datetime 與 calendar 模組都沒有移植過來, 只實作了 time 模組而已, 用 dir() 檢視 time 模組內容可知 time 模組甚至也沒有實作 ctime() 方法, 與日期時間操作有關的方法只有 time(), mktime() 與 localtime() 三個方法可用而已 :

>>> import time 
>>> dir(time) 
['__class__', '__name__', 'localtime', 'mktime', 'sleep', 'sleep_ms', 'sleep_us', 'ticks_add', 'ticks_cpu', 'ticks_diff', 'ticks_ms', 'ticks_us', 'time']

其中 time() 預設傳回 2000 年 1 月 1 日 0 時 0 分 0 秒起算的秒數 (稱為 epoch), localtime() 方法則傳回其 tuple 型態之日期時間; 而 mktime() 則反過來將 tuple 型態之日期時間轉回 epoch (秒數). 而 sleep() 則是最常用的延遲函數, 其傳入參數為秒數, 同理 sleep_ms() 為毫秒 (ms), 而 sleep_us() 為微秒 (us). 

>>> time.time()              #傳回 epoch 秒數
294
>>> time.localtime()      #傳回 RTC 的日期時間 tuple
(2000, 1, 1, 0, 5, 4, 5, 1)
>>> time.mktime(time.localtime())      #將日期時間 tuple 轉成 epoch 秒數
314

注意, time.localtime() 的傳回值定義如下 :

(年, 月, 日, 時, 分, 秒, 星期, 年日,日光節約時間)

其中年日為一年中的第幾天 (1~365/366); 星期為 0~6 的整數, 0 代表星期一, 1 代表星期二, .... 6 代表星期日,日光節約時間=0 表示無.

關於 time 模組之方法參考 :

https://docs.micropython.org/en/latest/library/utime.html#functions
Python time localtime()方法
http://www.runoob.com/python/python-date-time.html

事實上 ESP32 晶片內有一個儲存日期時間的即時時鐘 (Real Time Clock), 呼叫 time.localtime() 時就是從這個 RTC 取得日期時間資訊的. RTC 的存取函數放在 ESP32 內建模組 machine 的 RTC 類別中, 參考 :

https://docs.micropython.org/en/latest/library/machine.RTC.html

>>> import machine 
>>> dir(machine) 
['__class__', '__name__', 'ADC', 'DAC', 'DEEPSLEEP', 'DEEPSLEEP_RESET', 'EXT0_WAKE', 'EXT1_WAKE', 'HARD_RESET', 'I2C', 'PIN_WAKE', 'PWM', 'PWRON_RESET', 'Pin', 'RTC', 'SDCard', 'SLEEP', 'SOFT_RESET', 'SPI', 'Signal', 'TIMER_WAKE', 'TOUCHPAD_WAKE', 'Timer', 'TouchPad', 'UART', 'ULP_WAKE', 'WDT', 'WDT_RESET', 'deepsleep', 'disable_irq', 'enable_irq', 'freq', 'idle', 'lightsleep', 'mem16', 'mem32', 'mem8', 'reset', 'reset_cause', 'sleep', 'time_pulse_us', 'unique_id', 'wake_reason']
>>> machine.RTC                # RTC 是一個類別
<class 'RTC'>
>>> dir(machine.RTC)        # RTC 的成員
['__class__', '__name__', 'datetime', 'init', 'memory']

可見 RTC 類別有三個方法 :

 machine.RTC 物件方法 說明
 datetime([datetime]) 查詢或設定 RTC 日期時間 (傳入 list 或 tuple)
 init(datetime) 設定 RTC 日期時間 (傳入 list 或 tuple)
 memory([bytes]) 查詢或設定 RTC 記憶體儲存之資料 (bytes 類型)

注意, init() 與 datetime() 都可以傳入一個 8 元素的 tuple 或 list 來設定 RTC, 其定義為 : 

(年, 月, 日, 星期, 時, 分, 秒, 毫秒)   

其中星期為 0~6 的整數, 0 代表星期一, 1 代表星期二, .... 6 代表星期日, 例如 : 

>>> rtc = machine.RTC()                                   #建立 RTC 物件
>>> rtc.init((2019, 7, 10, 2, 14, 50, 0, 0))            #初始化 RTC
>>> rtc.datetime()                                                #查詢 RTC
(2019, 7, 10, 2, 14, 50, 1, 519033)
>>> rtc.datetime()                                                #查詢 RTC
(2019, 7, 10, 2, 14, 50, 6, 408877)
>>> rtc.datetime()                                                #查詢 RTC
(2019, 7, 10, 2, 14, 50, 9, 538456)
>>> rtc.datetime([2019, 7, 10, 2, 14, 50, 0, 0])    #設定 RTC
>>> rtc.datetime()                                                 #查詢 RTC
(2019, 7, 10, 2, 14, 50, 2, 339379)
>>> rtc.datetime()                                                 #查詢 RTC
(2019, 7, 10, 2, 14, 50, 8, 349071)

RTC 有一個 ROM 記憶體 (512 bytes) 可用 memory() 方法來儲存暫時的資料 (重啟會保留, 但拔電源會消失), 注意, 此記憶體要用二進位 bytes 型態儲存 :

>>> rtc.memory()                 #查詢 RTC 記憶體
b''
>>> rtc.memory(b'hello')    #設定 RTC 記憶體
>>> rtc.memory()                 #查詢 RTC 記憶體
b'hello'

由於 ESP32 進入睡眠模式時 RAM 記憶體中的資料可以儲存在 RTC 記憶體中, 等 ESP32 被喚醒時再從 RTC 記憶體中取出繼續執行. 關於睡眠模式參考 :

Insight Into ESP32 Sleep Modes & Their Power Consumption


由上可知, ESP32 的 RTC 模組必須經過初始化校準時間才能使用. 使 ESP32 擁有正確日期時間的方法是利用 MicroPython 內建的 ntptime 模組來與 NTP 伺服器同步, 此模組有一個 settime() 方法讓內部時鐘與網路上的 NTP 伺服器時間同步, 因此在呼叫 ntptime.settime() 之前要先讓 ESP32 的 STA 介面連上 Internet, 我將上一篇測試中的 main.py 程式加上 pre0() 與 now() 函數如下 :


#main.py
import network
import ubinascii
import time
import ntptime

def connect(ssid, pwd):
    sta.connect(ssid, pwd)
    print('Connecting to WiFi AP=', ssid, ' ...')
    time.sleep(8)
    if sta.isconnected():
        print('Connected: ', sta.ifconfig()[0])
    else:
        print('Can not connect to AP=' + ssid)

def disconnect():
    sta.disconnect()
    return True

def scan():
    aps=sta.scan()
    for ap in aps:
        ssid=ap[0].decode()
        mac=ubinascii.hexlify(ap[1], ':').decode()
        rssi=str(ap[3]) + 'dBm'
        print('{:>20} {:>20} {:>10}'.format(ssid, mac, rssi))

def ip():
    return sta.ifconfig()[0]

def pre0(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

def now():
    ntptime.settime()
    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),pre0(M),pre0(D),pre0(H),pre0(m),pre0(S))
    return t

ap=network.WLAN(network.AP_IF)
ap.active(True)
ap.config(authmode=4, password='micropythoN')
sta=network.WLAN(network.STA_IF)
sta.active(True)

上面新增的 pre0() 函數用來將個位數之月, 日, 時, 分, 秒前面冠上 '0' 補足為兩個字元以使日期時間格式整齊劃一, 而 now() 函數則用來把與 NTP 伺服器同步後之內部時鐘的日期時間以 "YYYY-MM-DD HH:mm:SS" 格式字串傳回. 這裡傳回的是台灣時間, 因為台灣時間是 UTC+8, 8 小時是 28800 秒 (8*60*60=28800), 所以 now() 函數中傳入 localtime() 的參數是 utc_epoch + 28800, 如果要傳回東京時間就要改為 utc_epoch + 32400, 因為東京時間是 UTC + 9.

將此修改後的 main.py 用 ampy 上傳到 ESP32 後按 Ctrl+D 重啟, 用 dir() 檢視記憶體中的物件如下 :

>>> dir() 
['now', 'ap', 'connect', 'ntptime', 'gc', 'sta', 'scan', 'bdev', 'time', 'ubinascii', '__name__', 'disconnect', 'webrepl', 'network', 'ip', 'pre0', 'uos']

首先須呼叫 connect() 連線 WiFi 熱點, 成功後再呼叫 now() 取得與 NTP 伺服器同步後的最新時間字串 :

>>> connect('TonyNote8','blablabla')   
Connecting to WiFi AP= TonyNote8  ...
I (92904) wifi: new:<11,2>, old:<11,0>, ap:<11,2>, sta:<11,0>, prof:11
I (92904) wifi: state: init -> auth (b0)
I (92904) wifi: state: auth -> assoc (0)
I (92914) wifi: state: assoc -> run (10)
I (93094) wifi: connected with TonyNote8, channel 11, bssid = 06:d6:aa:04:fc:4a
I (93094) wifi: pm start, type: 1

I (93094) network: CONNECTED
I (94154) event: sta ip: 192.168.43.177, mask: 255.255.255.0, gw: 192.168.43.1
I (94154) network: GOT_IP
Connected:  192.168.43.177
>>> now()        #取得與網路時鐘同步之日期時間
'2019-07-09 23:32:41

每次呼叫 now() 都會先用 ntptime.settime() 與 NTP 伺服器做時間同步, 因此可取得最新的網路時鐘. 注意, 務必先連上 AP 可通 Internet 後才可以呼叫 now(), 否則會出現如下 "list index out of range" 錯誤 :

>>> disconnect()      #離線
I (853654) wifi: state: run -> init (0)
I (853654) wifi: pm stop, total sleep time: 710885777 us / 760555821 us

I (853654) wifi: new:<11,0>, old:<11,2>, ap:<11,2>, sta:<11,0>, prof:11
True
>>> I (853664) wifi: STA_DISCONNECTED, reason:8

>>> now()               #離線狀態下無法取得網路時鐘
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "main.py", line 37, in getTime
  File "ntptime.py", line 30, in settime
  File "ntptime.py", line 18, in time
IndexError: list index out of range 


參考 :

[錦囊XV] NTP (網路校時) 服務時好時壞怎麼辦?
Python machine.RTC() Examples
http://www.1zlab.com/wiki/micropython-esp32/rtc/
Setting RTC From Internet?


2019-07-15 補充 :

bpi:bit 關於 ntptime 的說明文件 :

https://bpi-steam-docs.readthedocs.io/zh_CN/latest/mPython/docs/library/micropython/ntptime.html

2019-07-20 補充 :

由於 NTP 伺服器的 UDP 協定不保證能收到回應封包, 呼叫 ntptime.settime() 有時會出現錯誤, 因此要放在 try-except 裡面做例外處理, now() 函數修改如下 :

def now():
    from ntptime import settime
    try:
        settime()
    except: 
        pass
    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),pre0(M),pre0(D),pre0(H),pre0(m),pre0(S)) 
    return t

參考下面這篇最底下的補充 :

MicroPython on ESP32 學習筆記 (七) : socket 與網頁伺服器

沒有留言 :