2017年5月30日 星期二

2017 年第 21 周記事 : 端午祭祖備品檢查表

本周適逢端午連假 (下周六要補班). 我是周日下午才回鄉下, 因為要等菁菁補習數學回來. 順路到岳父的農舍去拿了兩串粽子, 回到家發現餐桌上又有兩串, 原來是高樹阿姨剛拿來的. 自從母親仙遊後, 家裡就沒有人會包粽子了, 都靠親友接濟, 尤其是高樹阿姨每年都送兩串過來, 而我過年卻忘記買箱水果過去, 真是過意不去.

今日祭祖時才發現忘記買鹼粽, 所以整理了一份端午節 check list 以備往後檢查 :
  1. 三牲等供品 :
    雞豬一周前跟榮發舅媽預定 (含煮好). 也可考慮跟市集的烤雞店訂烤雞. 魚罐頭一個, 泡麵一包, 水煮蛋三顆, 麻米果 2包, 米酒 2 瓶
  2. 粽子 : 鹼粽 * 6, 鹹粽一串
  3. 水果三種
  4. 金香各一串
小舅在我菜園種的玉米再不採會太老了, 下午全部一次採完砍掉, 煮了兩鍋還剩一大包, 只好冰在冰箱下周再用. 復採了一袋地瓜葉傍晚回高雄時順路拿給二姨媽, 她很高興.

昨天終於吃到已經兩年沒吃到的自家海頓芒果了, 那滋味真是令人回味. 過去一個月我努力套袋, 六月就要驗收成果了, 真期待. 今年六株波蘿蜜也豐收, 這兩周陸續有過路人詢問, 爸說已賣了 6 顆近 2000 元, 沒想到這幾年沒人要的波蘿蜜竟然鹹魚翻身哩! 不過龍眼就沒這麼好了, 都端午了還在發新芽, 擺明了今年不開花, 好像附近村里皆如此, 今年龍眼應該會大漲價.

今天天氣不穩定, 一整天斷斷續續下著不大不小的驟雨. 夏日的午後, 難得有這樣清風徐來的涼意, 坐在曬穀場前的屋簷下邊賞雨邊大啖西瓜, 真是說不出的快活.

2017年5月27日 星期六

MicroPython v1.9 釋出

今天在測試 ESP8266 時發現上傳 main.py 到 MicroPython 根目錄下後卻執行異常, 檢查檔案系統發現連 boot.py 也不見了, 我按照之前重建檔案系統的步驟 run 一遍, 結果還是不行. 到這地步看起來必須重燒韌體才行. 原來 MicroPython 的檔案系統也是會 crash 掉的.

連線 MicroPython 檢查是否有穩定的新版韌體釋出, 果然 5/26 推出了 1.9 的穩定版, 檔案稍大, 約 585KB 左右. 燒錄完畢開機 REPL 介面如下 :

MicroPython v1.9-8-gfcaadf92 on 2017-05-26; ESP module with ESP8266
Type "help()" for more information.
>>>
PYB: soft reboot
#6 ets_task(40100164, 3, 3fff829c, 4)
OSError: [Errno 2] ENOENT
MicroPython v1.9-8-gfcaadf92 on 2017-05-26; ESP module with ESP8266
Type "help()" for more information.
>>>

使用 ampy 查詢檔案系統, 發現這個 1.9 新版解決了 1.8.7 版預設沒檔案系統的問題了, 裡面已經有 boot.py 開機檔案 :

D:\test>ampy --port COM4 ls
boot.py

使用 get 指令將其抓回本地端, 發現其 boot.py 沒有開啟 webrepl :

D:\test>ampy --port COM4 get boot.py
# This file is executed on every boot (including wake-boot from deepsleep)
#import esp
#esp.osdebug(None)
import gc
#import webrepl
#webrepl.start()
gc.collect()

我將之前保存的 boot.py 上傳檔案系統, 蓋掉 v1.9 版預設之 boot.py :

#import esp
#esp.osdebug(None)
import gc
import webrepl
webrepl.start(password="123456")
gc.collect()

D:\test>ampy --port COM4 put boot.py

重新開機就可以使用 webrepl 了, 參考 :

# MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試

~~~ 20170528 補充 :

換 1.9 版韌體後發現執行 ampy 會出現 "could not enter raw repl" 錯誤 :

E:\test>ampy --port COM3 ls
b'#6 ets_task(40100164, 3, 3fff829c, 4)\r\n\r\n ets Jan  8 2013,rst cause:4, boot mode:(3,3)\r\n\r\nwdt reset\r\nload 0x40100000, len 32112, room 16 \r\ntail 0\r\nchksum 0xe0\r\nload 0x3ffe8000, len 1104, room 8 \r\ntail 8\r\nchksum 0x4d\r\nload 0x3ffe8450, len 3000, room 0 \r\ntail 8\r\nchksum 0x46\r\ncsum 0x46\r\n\x0e\xfc\x80\x0cl\x9c\x9e|\xfe\x82\x12rrnb\x8e\x8c\x0cl\xec\x12\x0c\x0cb\xecl\x0cb\xec\x8c\x9c\x9c\xe2\xe2\x0c\x8c\x0c\x0cb\x0c\x8elrl\x0el\x0cl\x9c\x9e|\xfe\x82\x12rrnb\x8e\x8cll\x9c\x1c\x82\x0c\x0cb\xec\x8c\x0cb\xec\x8c\x9c\x9c\xe2\x02\x12\x12\x02\x0cb\xec\x0c\x8elrl\x0e\x8c\x0cl\x9c\x9e|\xfe\x82\x12rrnb\x8e\x8cl\x0c\x9c\x1c\x92\x0c\x0cb\x8c\x0c\x0cb\xec\x8c\x9c\x9c\xe2\x02\x02\x8c\x0c\x0cb\xeclb\x0el\x8c\xe2\x02\xec\x12\x82n\xec\x92r\x82\xf2n|\xec\x0c\x8cl\x8eprl\x8c\xe2r\x92l\x8cl\x12\x8c\x0c\x0c\x0cl`\x02\x8c\xe2r\x92l\xecl\x12\x8c\x0c\x0c\x0cl`\x02\x8c\xe2r\x92l\x0c\x8e\x9e\x00\x8c\x0c\x0cll`\x02rl\x8e\x82rl\x8c\x9e\xe2\x8cb\x0c\x8c\x8cb\x8cb\x0cbr\x02\x02\xec\x8er\x0c\x12\x12b\xec\xf2n\xee\x80\x12nn\xe2\x10\x02\x0c\x02l\x92\xf2\x02\x0cl\x0c\x8c\x0cl\x8c\x8ell\x8e\x1e\x82\x8c\x8c\x8c\xec\x8e\x0el\x80\x02n\xfc\x00\x8c\x9e\xe2\x8cb\x0cl\x0cl\x8erp\x0c\x0c\x02\x02\x8c\x9e\xe2\x8cb\x0cl\x0c\x8cb\x1crlrlr\xf2n\x9c\xe2\x00\x8c\x0c\xec\x82\x12\x02b\xe0\x80\x8c\x8c\x0c\x8cl\xe2\x8c\x0c\xec\x8e\x1e\x02\x02\x8c\x8c\x92lb\xecb\xec\xf2n\x9c\x9e\x80\x80\x02b\xe0\x8c\x8c\x8c\x0c\x8cl\xe2\x8c\x0c\xec\x8e\x1e\x02\x02\x8c\x8c\x92lb\x0el\x0e\xec\x0cl\x9c\x9e|\xfe\x82\x12rrnb\x8e\x8c\x0cl\x9c\x9c\x0c\x0cb\x8c\x0c\x0cb\xec\x8c\x9c\x9c\x9c|\x90\x8c\x0c\x0cbl\x0clb\x0el\x02\x82\x82\x02ln\x9cl`\x02\x02\x82\x82\x02ln\x9cprl\x0e\x0c\x8el\x9c\x9e|\xfe\x82\x12rrnb\x8e\x8c\xfe#5 ets_task(40100164, 3, 3fff829c, 4)\r\nWebREPL daemon started on ws://192.168.4.1:8266\r\nWebREPL daemon started on ws://0.0.0.0:8266\r\nStarted webrepl in manual override mode\r\n\r\nMicroPython v1.9-8-gfcaadf92 on 2017-05-26; ESP module with ESP8266\r\nType "help()" for more information.\r\n>>> '
Traceback (most recent call last):
  File "c:\python36\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "c:\python36\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "C:\Python36\Scripts\ampy.exe\__main__.py", line 9, in <module>
  File "c:\python36\lib\site-packages\click\core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "c:\python36\lib\site-packages\click\core.py", line 697, in main
    rv = self.invoke(ctx)
  File "c:\python36\lib\site-packages\click\core.py", line 1066, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "c:\python36\lib\site-packages\click\core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "c:\python36\lib\site-packages\click\core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "c:\python36\lib\site-packages\ampy\cli.py", line 142, in ls
    for f in board_files.ls(directory):
  File "c:\python36\lib\site-packages\ampy\files.py", line 91, in ls
    self._pyboard.enter_raw_repl()
  File "c:\python36\lib\site-packages\ampy\pyboard.py", line 202, in enter_raw_repl
    raise PyboardError('could not enter raw repl')
ampy.pyboard.PyboardError: could not enter raw repl

只好使用 webrepl 或 mpfshell 了.

2017年5月25日 星期四

MicroPython on ESP8266 (八) : GPIO 測試

GPIO 是微控器與外部互動的窗口, 接感測器的為 Input, 用來偵測外部狀態或擷取外部資料, 可說是微控器的耳目; 而當 Output 時是作為致動器, 用來驅動 LED, 顯示器, 或繼電器等.

ESP8266 的 GPIO 埠有 16+1 個 (1 是指 ADC 輸入), 但因為 IC 接腳有限, 因此腳位大多作多工用途 (一腳多用), 例如 GPIO 1 被用作 REPL UART TX; GPIO 3 是 REPL UART RX, GPIO 16 被用來將晶片從深度睡眠中叫醒, 這三支腳不能做一般 IO 用, 而 GPIO 6~11 是用來連接 Flash 晶片等 SPI 介面, 也不能使用, 因此可用的 GPIO 埠只有 0, 2, 4, 5, 12, 13, 14, 15 共 8 個, 參考 :

# Technical specifications and SoC datasheets

不過深圳 AI Thinker 出品的 ESP8266 板有 12+ 種之多, 各板在接出 GPIO 腳數目上差異很大, 例如最簡單的 ESP-01 僅接出 GPIO 0 與 GPIO 2 兩個埠, 其中 GPIO 0 若在開機時被偵測到 Low, ESP8266 將進入韌體燒寫模式 (透過 UART), 否則 GPIO 0 即為一般的 GPIO 埠. ESP-12E 與 ESP-12F 模組有將全部 GPIO 接出 (ESP-12S 沒有), 而 ESP-05 與 ESP-10 則完全沒有接出 GPIO 埠, 參考 :

# ESP8266之GPIO功能、資源探索
# 認識UART、I2C、SPI三介面特性
# ESP8266板卡眾多,如何選擇?

正常使用時, GPIO 輸入腳最好用 2K~10K 上拉電阻接至 3.3V (使用 2K 對雜訊抑制力較佳), 不過 GPIO 腳內部都有內建上拉電阻, 可以透過軟體設定開啟, 參考 :

# Using ESP8266 GPIO0/GPIO2/GPIO15 pins
How to Choose Your ESP8266 Module

關於 GPIO 用法參考 MicroPython 教學文件 :

MicroPython Documentation (v1.8.6 PDF)
Quick reference for the ESP8266 : Pins and GPIO

本序列之前的測試文章參考 :

MicroPython on ESP8266 (二) : 數值型別測試
MicroPython on ESP8266 (三) : 序列型別測試
MicroPython on ESP8266 (四) : 字典與集合型別測試
MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
MicroPython on ESP8266 (六) : 檔案系統測試
# 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 (驅動程式)

常用的 ESP8266 開發板 D1 mini 的 GPIO 接腳如下 :


Source : DIYIOT



另一塊常用的 NodeMCU 接腳如下 :


Source : ESP8266 SHOP


控制 ESP8266 的 GPIO 腳需從 machine 模組匯入 Pin 類別 :

from machine import Pin  

Pin 類別定義了 Pin.OUT 與 Pin.IN 常數可用來設定 GPIO 之輸出入 :

p0=Pin(0, Pin.OUT)      #定義 GPIO 0 為輸出腳, 傳回 Pin 物件
p2=Pin(2, Pin.IN)          #定義 GPIO 2 為輸入腳, 傳回 Pin 物件

也可以匯入整個 machine 模組 :

import machine

但這樣 Pin 類別就要加上 machine 來存取了 :

p0=machine.Pin(0, machine.Pin.OUT)      #定義 GPIO 0 為輸出腳, 傳回 Pin 物件
p2=machine.Pin(2, machine.Pin.IN)          #定義 GPIO 2 為輸入腳, 傳回 Pin 物件

參考 :

# Adafruit : Digital Inputs

注意, 這裡 Pin() 建構式的第一個參數是 ESP8266 本身 GPIO 的腳位編號, 例如要設定 GPIO 0 就傳入 0. 有些板子例如 NodeMCU 會將輸出入腳另外編號例如 D0, D1, D2 ... 等, 必須將其轉換為對應之 GPIO 編號才可以. 由於 ESP8266 板子很多, MicroPython 無法一一照顧到每一種板子, 因此採用 ESP8266 的 GPIO 編號是最大公約數, 教學文件原文如下 :

"Note that many end-user boards use their own adhoc pin numbering (marked e.g. D0, D1, ...). As MicroPython supports different boards and modules, physical pin numbering was chosen as the lowest common denominator. For mapping between board logical pins and physical chip pins, consult your board documentation."

要在輸出腳輸出 High/Low 位準可以直接呼叫 Pin 物件的 high() 或 low() 方法, 也可以呼叫 value() 方法並傳入參數 1 或 0 :

p0.high()       #在 GPIO 0 輸出 High
p0.low()        #在 GPIO 0 輸出 Low
p0.value(1)   #在 GPIO 0 輸出 High
p0.value(0)   #在 GPIO 0 輸出 Low

也可以在定義腳位時以參數 value 設定預設之輸出位準 :

p0=Pin(0, Pin.OUT, value=1)      #定義 GPIO 0 為輸出腳並預設輸出值為 High
p0=Pin(0, Pin.OUT, value=0)      #定義 GPIO 0 為輸出腳並預設輸出值為 Low

ESP8266 每一個 GPIO 腳都有內建上拉電阻, 可以在設定為輸入腳時以 Pin.PULL_UP 常數作為第三參數來啟用, 這樣該輸入腳預設值為 High, 否則會因為浮接而呈現隨機值 :

p2=Pin(2, Pin.IN, Pin.PULL_UP)    #定義 GPIO 2 為輸入腳, 並啟用上拉電阻
swPin=p2.value()       #讀取輸入腳 p2 目前的輸入值
ledPin=p0.value()      #讀取輸出腳 p0 目前的狀態

Pin 物件的 value() 函數是兩用的, 有傳參數時為 setter, 用來設定輸出腳的位準; 如果沒有傳參數進去的話為 getter, 用來讀取輸出入腳的值. 注意, 無參數的 value() 方法不只是用在讀取輸入腳的位準而已, 也可以用在輸出腳, 這時是讀取該輸出腳目前之狀態.

下面這個測試 1 的程式會持續不斷每隔一秒讓接在 GPIO 2 腳的 LED 燈閃爍.

測試 1 : 持續閃爍的 LED (flashing_LED.py)

此測試是要讓接在 GPIO 2 上的 LED 持續閃爍, 我找了一顆 LED 與 220 歐姆的限流電阻串接在 GPIO 2 後接地, 然後在 PC 上編寫了一個測試程式如下 :

from machine import Pin    #匯入 Pin 類別
pin=Pin(2, Pin.OUT)          #定義 GPIO 2 為輸出腳
import time                         #匯入時間模組
while True:                         #無窮迴圈
    pin.high()                        #點亮 LED
    time.sleep(1.0)                #暫停 1 秒
    pin.low()                         #熄滅 LED
    time.sleep(1.0)                #暫停 1 秒

將程式存檔為 main.py (一定要用此名稱, 因為它是 MicroPython 開機跑完 boot.py 後要執行的程式), 然後用 ampy 將此程式傳到 ESP8266 的根目錄下 (注意, 使用 ampy 時 Putty 連線須關閉) :

D:\test>ampy --port COM4 put main.py      #上傳檔案

關於 MicroPython 的檔案系統管理, 參考 :

# MicroPython on ESP8266 (六) : 檔案系統測試

重開機就會看到 LED 不斷地閃爍.

間隔 1 秒不斷閃爍的 LED

這時若用 Putty 連線 ESP8266 將無反應, 因為目前程序是 main.py 裡的無窮迴圈, 若要中斷執行可按 Ctrl + C 就會送中鍵盤中斷, 控制權轉回 REPL 介面 :

Traceback (most recent call last):
  File "main.py", line 8, in <module>
KeyboardInterrupt:

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>>
>>>

要再次執行 main.py 可按 Ctrl + D 軟體開機即可.

>>>
>>>
PYB: soft reboot    (按了 Ctrl + D)
#6 ets_task(40100164, 3, 3fff8398, 4)
Traceback (most recent call last):
  File "boot.py", line 5, in <module>
  File "webrepl.py", line 70, in start
  File "webrepl.py", line 21, in setup_conn
OSError: [Errno 12] ENOMEM

下面測試 2 程式是從之前的 Arduino 程式改寫而來, 參考下列文章中的測試 1 :

Arduino 的按鈕開關測試 (一) : 輪詢法 (Polling)

測試 2 : 沒有處理按鈕的彈跳現象 (push_button_no_debouncing.py)

from machine import Pin
swPin=Pin(0, Pin.IN, Pin.PULL_UP)    #GPIO 0 設為輸入並開啟上拉電阻
ledPin=Pin(2, Pin.OUT)       #GPIO 2 設為輸出
ledState=0;                            #LED 初始狀態:暗的
while True:                            #無窮迴圈
    swState=swPin.value()     #讀取按鈕狀態
    if swState==0:                   #按鈕被按下 (接地)
        if ledState==1:              #若 LED 是亮的, 則熄滅之
            ledState=0
        else:                               #若 LED 是滅的, 則亮之
            ledState=1
        print(ledState)               #顯示 LED 狀態
        ledPin.value(ledState)   #更改 LED 狀態

在這個測試中我在 GPIO 0 上接一個按鈕開關, 開關的另一端接地, 然後將 GPIO 0 定義為輸入腳, 並開啟其上拉電阻, 這樣當按鈕沒按下時 GPIO 0 腳預設就是 High 而不是浮接的雜訊. 由於這個程式沒有處理按鈕的彈跳問題, 所以實際按按鈕並不會像期望中那樣按一下燈亮, 再按一下燈滅那樣交替明滅.

未處理彈跳-結果未達預期

處理按鈕彈跳問題可參考下列 Wemos D1 mini 教學文件 "2.4 Button" (page 10) :

Micropython on ESP8266 Workshop v1.0 (pdf)

我把程式改編如下 :

測試 3 : 處理按鈕彈跳現象之方法 (1) (push_button_with_debouncing_1.py)

import time  
from machine import Pin
swPin=Pin(0, Pin.IN, Pin.PULL_UP)
ledPin=Pin(2, Pin.OUT)
while True:
    if not swPin.value():      #若按鈕被按下 (接地)
        ledPin.value(not ledPin.value())     #反轉 LED 狀態
        time.sleep_ms(300)                        #暫停 0.3 秒
        while not swPin.value():                 #按鈕若還在按下狀態就在迴圈繞, 否則跳出去
            pass

此程式借助 time 模組的 sleep_ms() 函數來讓機器暫停執行 300 毫秒, 關於 sleep_ms() 參考 :

http://docs.micropython.org/en/v1.8.7/esp8266/esp8266/quickref.html#delay-and-timing
https://docs.micropython.org/en/latest/pyboard/library/utime.html#utime.sleep_ms

一般而言機械按鈕的不穩定彈跳時間約在 50ms~100ms 就會結束, 因此 300ms 過後應該已經到達 ON 或 OFF 的穩定狀態. 暫停 300ms 後用第二層無窮迴圈再次檢查按鈕狀態, 如果還在按下狀態就在迴圈裡繞維持現狀; 否則就跳出迴圈.

有處理彈跳-功能正常

注意, 上面的程式使用了 value() 函數讀取 GPIO 2 輸出腳以取得目前 LED 之明滅狀態 (傳回 1 或 0), 將狀態反轉後再傳給 value() 去設定 LED, 因此按一下明亮, 再按一次即熄滅. 由此可見 Python 語言之簡潔.

另外我在 MicroPython 官網找到一個給 Pyboard 用的消除彈跳程式, 參考 :

Debouncing a pin input (for Pyboard)

我將其改寫為 ESP8266 用的版本如下 :

測試 4 : 處理按鈕彈跳現象之方法 (2 (push_button_with_debouncing_2.py)

import time
from machine import Pin

def wait_pin_change(pin):              #等待位準變化趨於穩定的函數
    cur_value=pin.value()                 #進入函數時的目前腳位狀態
    counter=0                                    #計數器初始值 0
    while counter < 20:                 #到達穩定狀態後延遲 20ms 再離開函數的迴圈
        if pin.value() != cur_value:     #腳位狀態已改變 : 增量計數器
            counter += 1                        
        else:                                         #腳位狀態不變 : 計數器歸零
            counter = 0
        time.sleep_ms(1)                     #延遲 1ms

swPin=Pin(0, Pin.IN, Pin.PULL_UP)
ledPin=Pin(2, Pin.OUT)
while True:
    wait_pin_change(swPin)             #先至少等 20ms 開關趨於穩定
    if not swPin.value():                    #如果按鈕按下就反轉 LED 狀態
        ledPin.value(not ledPin.value())

此程式是利用自訂函數 wait_pin_change() 去偵測是否按鈕狀態有變化, 由於 GPIO 0 被上拉電阻預設為 High, 進入此函數後 cur_value 會記住 1, 如果按鈕沒被按下程序就會在無窮迴圈中不斷循環 (因 counter 不斷被 reset 為 0), 直到按鈕被按下, 這時 counter 就會增量, 但在下一迴圈若彈跳到 1, counter 又會被 reset 重來, 所以最快的話就是在預定地 20 次迴圈裡, 每次都讀取到彈跳時的 0, 讓 counter 每次都增量, 這樣 20ms 後就會跳出迴圈, 回到主程序, 但通常這不太可能發生. 總之, 在按鈕達到穩定狀態的 20ms 後就會離開 wait_pin_change() 函數, 這樣就可以反轉 LED 狀態了.

接下來要測試 ESP8266 的脈寬調變 PWM (Pulse Width Modulation) 功能, 這是利用數位方波的頻率與脈寬變化來模擬類比訊號如蜂鳴器聲音或 LED 亮度的方法, 參考 :

# Arduino 的聲音測試 (一)

PWM 主要的參數是方波的頻率 (frequency) 與工作週期 (duty cycle), 以 PWM 發出聲音來說, 頻率會決定聲音的音高 (pitch); 而工作週期則決定聲音的音色. 而以 PWM 訊號控制 LED 的話, 頻率會決定閃爍的程度; 而工作週期則決定亮度, 這亮度與 PWM 方波的平均電壓成正比 :

Vavg=Vcc * Th/T

此處 Th 為方波為 High 的時間, 而 T 為方波之週期. Th/T 即工作週期.

ESP8266 的每一個 GPIO 除了 GPIO 16 外都具有 PWM 功能, MicroPython 在 machine 模組中提供了 PWM 類別來處理脈寬調變, 通常會與 Pin 類別一起匯入 :

from machine import Pin, PWM

呼叫 PWM 類別的建構式 PWM() 並傳入 Pin 物件即可建立一個 PWM 物件, 例如 :

pwm2=PWM(Pin(2))  

注意, 不必傳入第二參數 Pin.OUT, 因 PWM 本來就是要做輸出用的.

這樣就可以呼叫 PWM 物件的 freq() 與 duty() 方法, 分別傳回目前方波的頻率與工作週期 :

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>> from machine import Pin, PWM
>>> pwm2=PWM(Pin(2))   #設定 GPIO 2 為 PWM 輸出
>>> pwm2.freq()                 #傳回目前頻率
500  
>>> pwm2.duty()                #傳回目前工作週期
0

可見方波預設頻率是 500 Hz, 而工作週期預設為 0. 注意, freq() 與 duty() 沒有傳參數時為 getter方法, 有傳參數時為 setter 方法, 因此如果要更改頻率與工作週期就傳入參數, 其範圍為 :
  1. freq() : 0~1000 Hz
  2. duty() : 0~1023  (1023 為 100% 工作週期)
例如下面這兩個設定指令會讓 LED 以一半的亮度與 20Hz 頻率閃爍, 一般來說頻率在 50Hz 以上幾乎感覺不出 LED 有在閃爍 :

>>> pwm2.duty(512)     #工作週期為 512/1024=50%
>>> pwm2.freq(20)        #20 Hz

但是如果把工作週期設為 1023, 則即使頻率為 20 Hz 也不會閃爍, 因為此時工作週期為 100%, 相當於一直輸出 High, 因此 LED 會恆亮.

事實上建構式 PWM() 可接受三個參數, 除了第一參數固定為 Pin 物件外, 可以將頻率與工作週期以 freq 與 duty 具名傳入, 例如 :

pwm2=PWM(Pin(2), freq=20, duty=512)

上面偵測按鈕被按下的方法稱為輪循法 (Polling), 偵測動作的程式碼擺在主迴圈裡, 時時刻刻都要去偵測, 比較耗費時間. 另外一種偵測外部狀態變化的方法稱為硬體中斷 (IRQ), ESP8266 的每一個 GPIO 腳都可以啟動 IRQ 功能, 讓 GPIO 腳位自己去監視狀態變化, 當觸發 IRQ 時就去執行中斷處理函數, 這樣主迴圈就不用花時間去關注輸入腳的狀態了. MicroPython on ESP8266 的 IRQ 功能是透過 Machine.Pin 物件的 irq() 方法來設定, 參考 :

http://docs.micropython.org/en/latest/wipy/library/machine.Pin.html#machine.Pin.irq
Arduino 按鈕開關測試 (二) : 硬體中斷法 (Interrupt)

呼叫 irq() 方法時必須傳入兩個必要參數 trigger (觸發方式) 與 callback (中斷處理函數名稱), 其中 trigger 可用的方式有如下常數 :
  1. Pin.IRQ_LOW_LEVEL (LOW 觸發)
  2. Pin.IRQ_HIGH_LEVEL (HIGH觸發)
  3. Pin.IRQ_RISING  (上升緣都觸發)
  4. Pin.IRQ_FALLING (下降緣都觸發)
這四種觸發方式可以用 | (或) 運算子組合, 例如 :

Pin.IRQ_RISING | Pin.IRQ_FALLING (上升或下降緣都觸發)
Pin.IRQ_LOW_LEVEL | Pin.IRQ_FALLING (LOW 或下降緣都觸發)

2017-09-05 註 :

IRQ_LOW_LEVEL 與 IRQ_HIGH_LEVEL 在 ESP8266 版的 MicroPython 尚未支援, 僅 RISING 與 FALLING 可用.


另外, 定義中斷處理函數時必須傳入一個參數例如 :

def int0(p):

因為當中斷發生時, MicroPython 會向中斷處理函數傳遞一個必要參數, 即發生中斷的腳位編號例如 Pin(0), 若定義時沒有宣告此參數, 則中斷發生時會因傳不進去而產生錯誤.


測試 5 : 處理按鈕彈跳現象之方法 (3 (push_button_with_debouncing_3.py)

from machine import Pin
import time

p0=Pin(0, Pin.IN, Pin.PULL_UP)
p2=Pin(2, Pin.OUT)
state=0               #LED 狀態旗標

def int0(p):
    global state     #取用全域變數
    if state:            #反轉 LED 狀態
        state=0
    else:
        state=1

p0.irq(trigger=Pin.IRQ_FALLING, handler=int0)   #設定 GPIO 0 外部硬體中斷 (下降緣)

while True:
    p2.value(state)  #依據狀態旗標改變 LED 狀態

實際測試結果功能大致正常, 但爾而會失常, 可見 IRQ 不會處理彈跳. 下列測試 6 程式是我參考以前 Arduino 的去彈跳程式改寫而來 :

測試 6 : 處理按鈕彈跳現象之方法 (4 (push_button_with_debouncing_4.py)

from machine import Pin
import time

p0=Pin(0, Pin.IN, Pin.PULL_UP)
p2=Pin(2, Pin.OUT)
state=0
debounceDelay=300                      #彈跳期
lastMillis=0                                    #紀錄最近時戳

def debounced():                             #去彈跳函數
    global debounceDelay                #取用全域變數
    global lastMillis                          #取用全域變數 (最近時戳)
    currentMillis=time.ticks_ms()    #取得目前時戳
    if (currentMillis-lastMillis) > debounceDelay:     #目前時戳與最近時戳差已超過彈跳期
        lastMillis=currentMillis          #將最近時戳更新為目前時戳
        return True                            
    else:
        return False

def int0(p):
    global state
    if debounced():       #已經過了彈跳期
        if state:                #反轉 LED 狀態
            state=0
        else:
            state=1
    print(p,"IRQ triggered ",state)    #印出

p0.irq(trigger=Pin.IRQ_FALLING, handler=int0)

while True:
    p2.value(state)

實測確實能完美地去彈跳, REPL 輸出畫面如下 :


去彈跳處理方法大致如上, 下面測試 7 是一個 PWM 有趣的應用-呼吸燈, 這是我從下面這個網站的 Breathing LED 範例改編而來的, 此程式使用了 machine 模組中 計時器類別 Timer, 利用它週期性呼叫回呼函數來遞增與遞減 PWM 的工作週期, 讓 LED 看起來像是在呼吸一樣.

https://www.dfrobot.com/blog-606.html

測試 7 : 會呼吸的 LED 燈 (breathing_LED.py)

from machine import Pin, Timer, PWM
pwm=PWM(Pin(2),100)   #GPIO 2 PWM 頻率 100 Hz
polar=0                              #極性 : 0=由暗漸亮 1=由亮漸暗
duty=0                               #工作週期 : 0~1023

def setLed(t):                     #計時器的回呼函數 (callback)
   global duty, polar            #存取全域變數
   if polar == 0:                   #極性 : 0=由暗漸亮
       duty += 16                   #工作週期遞減
       if duty >= 1008:     #不碰頂才不會轉折時頓一下 (1008=1024-16)
           polar=1                    #將碰頂時極性反轉
   else:                                 #極性 : 1=由亮漸暗
       duty -= 16                    #工作週期遞增
       if duty <= 0:            #到底時極性反轉
           polar=0
   pwm.duty(duty)               #更新工作週期

tim=Timer(-1)                     #使用 ID=-1 的虛擬計時器
tim.init(period=10, mode=Timer.PERIODIC, callback=setLed)    #啟動計時器
#後面 try~except 部分不用亦可
try:
   while True:
       pass
except:
   tim.deinit()                       #關閉計時器
   pwm.deinit()                    #關閉 PWM


上面程式中傳入 Timer() 建構式的參數 -1 是虛擬計時器之編號, 可以任意編定一個例如 -2 或 -3 的 ID. Timer 的 mode 有 Timer.ONE_SHOT 與 Timer.PERIODIC 兩種, 參考 :

# ESP8266 micro python - Tutorial 3: Micropython Timer I2C
# class Timer – control hardware timers
class Timer – control internal timers
http://docs.micropython.org/en/v1.8.7/esp8266/esp8266/quickref.html#timers

這個程式可以加上網路功能, 透過雲端主機偵測遠端的設備是否運作正常 (Hearbeat). 

參考 :

ESP8266 Tutorial: Programming the Onboard GPIO Pins
How to Use the ESP8266-01 Pins
How to Unbrick an ESP8266 – Using ESP-03 As Example
# ESP8266: Controlling a buzzer

2017-08-03 補充 :

關於 PWM 用法在下面這篇中也有用到 :

# MicroPython on ESP8266 (十六) : 蜂鳴器測試


2017-09-05 補充 :

在新版韌體中 Pin 物件已無 high() 與 low() 方法, 必須用 value(1) 與 value(0) 才行. 另外, 在 ESP-12 模組, WeMOS D1 Mini, 或 NodeMCU 板子上有內建一個接在 GPIO 2 的 LED 可用來顯示程式執行狀態, 例如尚未連線成功快閃, 連線成功後改為慢閃等. 注意, GPIO2 在 ESP8266 晶片內有 10K 歐姆上拉電阻, 因此要輸出 value(0) 才是亮燈, 不是 value(1) :

from machine import Pin
p2=Pin(2, Pin.OUT)
p2.value(1)    #輸出 High 位準 (LED 燈滅)
p2.value(0)    #輸出 Low 位準 (LED 燈亮)

ESP-12F 模組與轉接板焊接

去年六月跟露天賣家 alen_6833 買了一組 ESP-12F + 轉接板 (90 元), 原本想用來燒錄 NodeMCU, 但一直沒時間動手 :

# 向 allen_6833 採購電子零件模組一批
# ESP8266 ESP-12F 串口WIFI 無線模組 送轉接板+IC ESP-12E進階版 Arduino

事實上 ESP8266 的模組很多, 從 ESP-01 到 ESP-14 各種規格都有, 參考 :

# esp8266-module-family

其中我覺得 ESP-12 蠻適合取代 Arduino 當作物聯網的終端微控器, 理由 :
  1. 接出 9 個 GPIO 埠 (GPIO0, 2, 4, ,5, 12, 13, 14, 15, 16), 比 ESP-01 多出 7 個埠 
  2. 內建 4M Flash, 比新版 ESP-01 的 1MB 還多了 3MB
不過 ESP-12 需要一個轉接板將其接腳接出, 才能在麵包板上做實驗. 這個轉接板與 ESP-07 的一樣, 參考 :

How to Choose Your ESP8266 Module

我買的這片還附了一顆 AMS1117 可焊在背面, 這樣 VCC 就可以用 5V 電源, 但是必須將正面三顆電阻中間那顆移除, 否則還是會灌 5V 到 ESP8266 上可能造成 ESP8266 損壞, 賣家的回應如下 :

"附贈轉接板默認輸入(VCC) 3.3V使用,如果要用5V(VCC)要將附贈的5V轉3.3V的IC焊上,並將IC背面的電阻移除(3個電阻的中間那個電阻),沒有移除會造成IC無作用(輸出還是5V)有可能會造成模組損壞"

"穩壓 ic 不焊的話只可用3.0v~3.7V 有焊可5v"

這實在有點麻煩, 所以我決定不焊 AMS1117 了. 同樣先用三秒膠固定兩邊的針腳與 ESP-12 模組, 針腳還好焊, 但模組的郵票孔針腳卻不容易焊, 我看烙鐵還很好哇, 怎會焊不上去? 換了一個新烙鐵條還是一樣, 難道是焊錫的問題? 翻了一下零件盒, 發現還有一捲之前買來備用的較貴的焊錫輪, 改用這個焊錫之後就能順利上錫了, 果然一分錢一分貨, 以後不要再買便宜的焊錫了.

下方便宜的焊錫管 ~~~ 以後不要買了

焊接完成後用 PL2303HX 連接到 PC 的 USB, 接法與 ESP-01 模組一樣 :

PL2303HX       
+5V(紅線)  - 不可接 (會讓 ESP8266 燒毀)
GND(黑線)-  接 ESP8266 與 3.3V 電源板的 GND (三個 GND 共接)
TXD(綠線) -  接 ESP8266 的 URXD
RXD(白線)-  接 ESP8266 的 UXTD

然後 ESP-12 模組的 CH_PD 要接 3.3V, GPIO 0 要接 GND 才能進行 MicroPython 的燒錄. 注意, 地線共接很重要, 燒錄程式無反應通常都是 GND 沒有共接造成的.

燒錄 MicroPython 韌體結束後拔掉 GPIO 0 的接地線, 開機 OK! 可以進入 REPL 介面.


不過我發現 ESP-12 的轉接板兩邊的接腳寬度剛好跨在麵包板兩端, 這要接線實在很不方便, 應該像 NodeMCU 那樣細長些才好用. 我只好把板子翻過來, 改用杜邦線連接針腳.

ESP-12 有 ESP-12S, ESP-12E, 以及 ESP-12F, 我買的是 ESP-12F, 此款為天線優化版, 通訊距離比 ESP-12E 增加 30%-50%. ESP-12S 是比較後出的版本, 與 ESP-12E 不同之處是底下的 GPIO9, GPIO10, MISO, MOSI 等六支腳不再接出, 因為其中的 GPIO 也不能用, 例如 Adafruit 的說明 :

"we are shipping the ESP-12S version which no longer has the SPI flash pads brought out on the bottom row. Note that you couldn't use them for GPIOs or anything, they were there so you could program the FLASH chip only."

參考 :

# ESP-12 Series 比較
# 【選擇指南】ESP8266板卡眾多,如何選擇?
ESP8266 串口 WIFI (ESP-12F)
# ESP8266 esp-12s 工業級 ESP-12F 全套 WIFI無線模組 + 轉接板 + IC 全套 $79

除了用 ESP-12 來獲得更多 GPIO 外, 還可以使用 NodeMCU 或 WeMOS D1 mini, 好處是板上內建 USB 晶片與 Micro USB 插槽, 不需使用 USB-TTL 轉接線. 我比較喜歡 Wemos D1 mini, 這款板子跟我買的第一個 Arduino Nano 很像, 其教學文件如下 (也可用在 ESP-12 模組) :

MicroPython on Wemos D1 mini 教學文件

還有一塊接出全部 IO 腳的 ESP-201 板 :

# 【傑森創工坊】ESP8266 ESP-201 全IO引出 WIFI 無線模組 WIF 收發無線模組 $180

參考 :

# First steps with Micropython on a NodeMCU
# Getting started with MicroPython on the ESP8266
# 在 NODEMCU 上執行 MICROPYTHON
# NodeMCU-compatible pin numbering
# nodeMcu API说明

好站 : Rsspberry Pi 溫濕度顯示器

今天在 Google+ 看到社群有人分享樹莓派的溫溼度顯示器 :

Weather According to Raspberry Pi - WARP

詳細內容參考其部落格 :

http://programmingprof.blogspot.tw/2017/05/warp.html

作者使用樹莓派 Raspbian 的 SenseHat 模擬器與 Flask 框架來展示溫溼度數據, 原始碼可在下面網址下載 :

http://programmingprof.blogspot.tw/2017/05/warp.html

2017年5月23日 星期二

2017 年第 20 周記事

因為週日 5/21, 周一 5/22 要去合歡山賞杜鵑, 所以周六就跟二哥先回鄉下了, 他突然說考完段考了, 所以週六晚上想跟補習班請假跟我回鄉下.

週日十點半才出發實在有點晚, 因為還要繞到鳳山去接小姨子, 上交流道已經十一點了, 只在古坑休息站停留吃午餐, 就直接經國六至埔里, 然後上清境, 到青青草原時想停下來逛, 但沒地方停車, 只好先到雲上太陽 checkin 後再走下來, 回到青青草原已經四點, 離閉園只剩一小時, 只買了兩頂帽子, 吃過羊奶冰棒就走回民宿了. 開這麼遠的路其實應該早一點出發才對.

周一早上七點半啟程前往東峰, 原定行程是只爬半山腰就好, 但是途中下山的人不斷地說峰頂杜鵑較多, 於是改變計畫攻頂, 果然越往上走花開得越多, 雖然走一小段路大家就氣喘如牛, 但在運用心理戰激勵之下, 終於在 11 點登頂. 王維所謂 "行到水窮處, 坐看雲起時", 雖意在勵志, 但用來寫景抒情, 我想應該就是這樣的意境吧!


2017年5月21日 星期日

合歡山賞花

去年底與老同學四人去爬合歡山時, 老張就說五月可再來賞高山杜鵑, 本來想邀爸一起去, 但考量到要爬一些山路, 所以就只有我, 水某, 菁菁參加, 周一請假一天. 這是老張寄來的合歡山賞花行程 :

 合歡群峰賞花行程: 5/21:  12:30出發(假日易塞車,若能提早到也可四處走走)  14:00 古坑服務區集合  17:00清境  18:00~19:00晚餐(晚餐只供應這時段,錯過吃自己)  19:00~22:30 Free Time  20:30~晚安 5/22: (以賞花為主,行程視狀況調整)  06:00起床  06:20早餐  07:00出發  07:50~09:30合歡東峰(半山腰)  09:30~11:00小奇萊(0.7K)  11:00~12:00 石門山+合歡尖峰(半山腰)  12:00~14:00合歡北峰(0.5K)  14:00~15:00折返  15:00~15:30清境休息  15:30~20:00高雄  攜帶物品  禦寒衣物(每1000公尺降6度,不要以山下溫度衡量高山上氣溫)  防曬乳、墨鏡  雨具:雨衣或雨傘  午餐自備  飲用水  小背包  零食  相機  登山杖  個人藥物(如普拿疼,OK繃)

晚上住宿清境雲上太陽田園民宿, 老張幫我們訂四人房樓中樓, 參考 :

http://aerysun.okgo.tw

電話:訂房專線0932-593961 地址:南投縣仁愛鄉仁和路210-6號

上一次去清境農場是小狐狸小學時代, 當時是作為帶媽媽與大阿姨去台北玩的中途站, 大概是八年前了吧! 那時是住某義大利民宿, 我印象最深刻的是棉被一打開就狂打噴嚏, 塵螨驚人啊!

太久沒去中部了, 雖有導航, 但查看一下地圖還是必要的, 應該是走國三過草屯後, 在霧峰轉 6 號水沙連到埔里後接 14 號埔霧公路.



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?

2017年5月18日 星期四

MicroPython on ESP8266 (六) : 檔案系統測試

昨晚重建 MicroPython on ESP8266 的檔案系統後就順利搞定 WebREPL 檔案傳送這個重要關卡了, 實在是可喜可賀. 那麼今天就來好好地測試一下檔案系統的功能. 由於 WebREPL client 對於檔案的管理只有上傳與下載檔案的功能而已, 如果要對檔案系統進行管理, 例如新增刪除目錄等等就力有未逮, 必須使用 ampy, mpfshell, 或 rshell 等套件. 在上一篇文章裡只大略測試了 ampy 與 mpfshell 的檔案管理功能, 這裡要來做個詳細測試.

此系列前面五篇測試參考 :


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 (驅動程式)

其實檔案系統最重要的是根目錄 / 下的兩個 python 執行檔 boot.py 與 main.py, 韌體在開機後首先會去根目錄下找尋 boot.py, 有找到的話就先執行此開機檔, 然後再尋找 main.py, 有的話就執行此程式, 一般而言, main.py 裡面主要就是一個執行特定應用邏輯無窮迴圈. 因此通常會在 boot.py 裡面進行系統設定, 例如開啟關閉 debug 或 webrepl 功能等. 一般 boot.py 的內容如下 :

#import esp
#esp.osdebug(None)
import gc
import webrepl
webrepl.start(password="123456")
gc.collect()

首先在 REPL 或 WebREPL 介面上測試 MicroPython 的檔案功能, 注意, 以下測試是在新版 (1MB Flash) 的 ESP-01 模組上進行的, 因為只有 Flash 記憶體超過 1MB 才有足夠空間建立檔案系統, 參考 :

# The internal filesystem

"If your devices has 1Mbyte or more of storage then it will be set up (upon first boot) to contain a filesystem. This filesystem uses the FAT format and is stored in the flash after the MicroPython firmware."

MicroPython 事實上只納入基本的檔案目錄操作功能如檔案與目錄的讀寫而已, 雖有實作 os 模組但卻未實作 os.path 模組, 例如 :

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

一. 讀寫文字檔 :

Python 的檔案操作是使用內建函數 open() 與 close(), 此函數會開啟指定檔案, 並傳回其檔案物件參考, 語法如下 :

 函數 & 方法 說明
 f=open('filename' [, 'mode']) 開啟檔案 (預設唯讀 mode='r'), 傳回檔案物件參考
 f.close() 關閉參考 f 之所指之檔案物件 (關檔)

其中 filename 為文字檔案名稱如 data.txt, data.log 等, 而 mode 為開啟模式, 有三種模式 :

 開啟模式 說明
 w 寫入模式 (取代原檔案內容)
 a 附加寫入模式 (寫入資料附加於原內容尾端)
 r 唯讀模式 (預設)

開啟檔案後可用下列檔案物件的方法讀寫檔案 :

 方法 說明
 write(string) 將字串 string 寫入檔案 
 read() 傳回全部檔案內容
 readline() 逐列讀取檔案內容, 傳回目前指標所指之列內容
 readlines() 逐列讀取檔案內容, 將各列內容放在串列中傳回

在 w 模式開啟一個不存在的檔案相當於是建立一個空白檔案, 利用傳回的檔案物件參考呼叫 write() 方法就可以寫入文字資料, write() 方法的傳回值是寫入的 byte 數. 如果要開啟的檔案已存在, 則 w 模式下用 write() 寫入的資料將取代檔案內的原資料 (原檔案內容會被刪除); 而在 a 模式下原檔案內容不會消失, 新寫入的資料會附加在原檔案內容後面.

注意, 檔案讀寫完畢務必呼叫 close() 方法關閉檔案, 否則記憶體將無法釋放而造成系統資源浪費, 例如.

>>> f=open('data.txt','w')   #開啟一個空白檔案                                     >>> f.write('Hello!\r\n')     #寫入資料
8                                                         #傳回寫入 byte 數
>>> f.write('World!')         #寫入資料
6                                                         #傳回寫入 byte 數
>>> f.close()                       #關閉檔案

這時用 os.listdir() 就可以看到檔案系統中多出了剛建立的 data.txt :

>>> import os                                                                                         
>>> os.listdir()                                                                                      
['boot.py', 'main.py', 'data.txt']

在 WebREPL 網頁右端的 "Get a file" 框內輸入 data.txt 後按 "Get from device" 鈕即可下載剛剛建立的文字檔 :


開啟所下載之檔案, 其內容為兩列文字 (因為有跳行字元 \r\n 之故) :

Hello!
World!

注意, Windows 跳行須用 \r\n, 若只用 \n 沒有跳行效果. 

如果以 a 模式再次開啟檔案, 並寫入 'MicroPython' 字串, 則此字串將附加於 World! 之後, 例如 :

>>> f=open('data.txt','a') 
>>> f.write('MicroPython') 
11 
>>> f.close()

再次下載 data.txt 開啟後發現內容果然變成如下 :

Hello!
World!MicroPython

讀取檔案內容可使用 read(), readline(), 以及 readlines() 這三個方法. 其中 read() 會一次讀取全部內容後傳回, 例如 :

>>> f=open('data.txt','r')                                                                            
>>> f.read()                                                                                          
'Hello!\r\nWorld!MicroPython'

注意, 以 r 模式開啟檔案時, 該檔案必須已存在, 否則會產生錯誤 :

>>> f=open('data.log','r')                                                                            
Traceback (most recent call last):                                                                    
  File "<stdin>", line 1, in <module>                                                                 
OSError: [Errno 2] ENOEN

其次, readline() 是每次讀取檔案中的一列後傳回 (字串), 讀到檔尾時會傳回空字串 : 

>>> f=open('data.txt','r')
>>> line=f.readline()       #讀第 1 列
>>> line                    
'Hello!\r\n'
>>> line=f.readline()       #讀第 2 列
>>> line
'World!MicroPython'
>>> line=f.readline()       #已到檔尾, 傳回空字串
>>> line
''
>>> f.close()

可以用 while 迴圈逐列讀取, 用傳回值是否為真來控制迴圈是否繼續, 因空字串之布林值為 False, 固可作為迴圈終止條件 :

>>> f=open('data.txt','r')
>>> while True:    #無限迴圈
...     line=f.readline()           #讀取一列
...     if not line:break            #檔尾傳回空字串 (False) 跳出迴圈
...     print(line)
...
Hello!

World!MicroPython
>>> f.close()

readlines() 方法也是一次讀取全部內容, 但它是逐列讀取後放入串列中傳回, 可以使用 for 迭代將串列內容讀出來, 例如 :

>>> f.close()
>>> f=open('data.txt','r')
>>> lines=f.readlines()
>>> type(lines)
>>> for line in lines:
...     print(line)
...
...
...
Hello!

World!MicroPython
>>> f.close()

以上便是 MicroPython 的基本檔案讀寫測試. MicroPython 的檔案系統可以應用在物聯網的 data logger, 例如紀錄溫溼度等環境氣候變化等.


二. 檔案管理套件 :

由於 WebREPL 只有檔案上傳下載功能, 如果需要新增刪除目錄或檔案等檔案管理功能, 需另行安裝 Adafruit-ampy, mpfshell, 或 rshell 等套件才可達到此目的.


1. Adafruit-ampy :

此套件由 Adafruit 發布, 利用串列埠與 ESP8266 模組連線 (沒有 wifi 連線功能) 進行檔案管理, 安裝 ampy 參考 :

Adafruit : Install ampy

利用 pip 指令即可線上安裝此套件 :

D:\<pip3 install adafruit-ampy
Collecting adafruit-ampy
  Downloading adafruit_ampy-1.0.1-py2.py3-none-any.whl
Collecting click (from adafruit-ampy)
  Downloading click-6.7-py2.py3-none-any.whl (71kB)
Collecting pyserial (from adafruit-ampy)
  Downloading pyserial-3.3-py2.py3-none-any.whl (189kB)
Installing collected packages: click, pyserial, adafruit-ampy
Successfully installed adafruit-ampy-1.0.1 click-6.7 pyserial-3.3

可見 ampy 有兩個相依套件 click 與 pyserial, 如果要在無 Internet 環境安裝 ampy, 可在下列網址下載這三個檔案以離線方式安裝 :

https://pypi.python.org/pypi/adafruit-ampy/
https://pypi.python.org/pypi/click
https://pypi.python.org/pypi/pyserial

在命令提示字元視窗下 ampy --help 會列出 ampy 命令格式 :

D:\test>ampy --help
Usage: ampy [OPTIONS] COMMAND [ARGS]...

  ampy - Adafruit MicroPython Tool

  Ampy is a tool to control MicroPython boards over a serial connection.
  Using ampy you can manipulate files on the board's internal filesystem and
  even run scripts.

Options:
  -p, --port PORT  Name of serial port for connected board.  Can optionally
                   specify with AMPY_PORT environemnt variable.  [required]
  -b, --baud BAUD  Baud rate for the serial connection (default 115200).  Can
                   optionally specify with AMPY_BAUD environment variable.
  --version        Show the version and exit.
  --help           Show this message and exit.

Commands:
  get    Retrieve a file from the board.
  ls     List contents of a directory on the board.
  mkdir  Create a directory on the board.
  put    Put a file or folder and its contents on the...
  reset  Perform soft reset/reboot of the board.
  rm     Remove a file from the board.
  rmdir  Forcefully remove a folder and all its...
  run    Run a script and print its output.

這裡很重要的是 OPTION 中的 port, 在 Windows 是用 COMx (慣例是用大寫), 所以要先在裝置管理員中確定 USB-TTL 轉接線是哪一個 COM 埠. 另外因為 ampy 要使用串列埠, 所以如果有使用 PuTTY 開啟該 COM 埠的話, 必須關掉 Putty, 否則 ampy 的指令都會報錯, 例如 :

D:\test>ampy --port COM4 put main.py
Traceback (most recent call last):
  File "c:\python36\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "c:\python36\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "C:\Python36\Scripts\ampy.exe\__main__.py", line 9, in
  File "c:\python36\lib\site-packages\click\core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "c:\python36\lib\site-packages\click\core.py", line 697, in main
    rv = self.invoke(ctx)
  File "c:\python36\lib\site-packages\click\core.py", line 1063, in invoke
    Command.invoke(self, ctx)
  File "c:\python36\lib\site-packages\click\core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "c:\python36\lib\site-packages\click\core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "c:\python36\lib\site-packages\ampy\cli.py", line 70, in cli
    _board = pyboard.Pyboard(port, baudrate=baud)
  File "c:\python36\lib\site-packages\ampy\pyboard.py", line 143, in __init__
    raise PyboardError('failed to access ' + device)
ampy.pyboard.PyboardError: failed to access COM4    (因為 PuTTY 也在連線 COM4 導致衝突)

首先來測試目錄的新增 (mkdir) 刪除 (rmdir), 檔案刪除 (rm), 以及檔案與目錄列示 (ls) 等指令 :

D:\test>ampy --port COM4 mkdir test      #建立目錄 test

D:\test>ampy --port COM4 ls                     #列出根目錄下的檔案與目錄
boot.py
main.py
data.txt
data.log
test                                                                      #新增的目錄

D:\test>ampy --port COM4 rmdir test       #刪除目錄 test

D:\test>ampy --port COM4 ls                      #列出根目錄下的檔案與目錄
boot.py
main.py
data.txt
data.log

D:\test>ampy --port COM4 rm data.log     #刪除檔案 data.log

D:\test>ampy --port COM4 ls                      #列出根目錄下的檔案與目錄
boot.py
main.py
data.txt                                                                 #data.log 已被刪除

其次測試 get 指令, 我原以為這與 webrepl 一樣是從 ESP8266 下載檔案, 測試結果卻是讀取檔案內容後直接在命令列, 我覺得應該稱為 cat 才對 :

D:\test>ampy --port COM4 get data.txt      #讀取並顯示檔案內容
Hello!
World!MicroPython

接下來測試檔案上傳 (put) 指令, 為了同時也測試 run 指令, 我先編輯了一個執行檔 hello.py, 就是單純地印出 Hello World! 而已 :

print('Hello World')

然後將此 hello.py 上傳到根目錄

D:\test>ampy --port COM4 put hello.py      #上傳檔案  

D:\test>ampy --port COM4 ls                       #列出根目錄下的檔案與目錄
boot.py
main.py
data.txt
hello.py

D:\test>ampy --port COM4 run hello.py     #執行 hello.py
Hello World

注意, run 指令會等 python 程式執行完才會輸出結果, 這對如下有無限迴圈的程式而言, 用 ampy run 將永遠看不到所執行程式之輸出結果 :

i = 1
while True:  #無限迴圈
    print(i)     #ampy run 永遠無法看到此輸出
    i += 1

解決之道是加上 --no-output 參數 :

D:\test>ampy --port COM4 run endlessloop.py  --no-output   #執行 hello.py

參考 :

Run Code

最後測試重啟指令 reset :

D:\test>ampy --port COM4 reset  

D:\test>

此指令下達後視窗會停住一會兒等系統重置完成才會跳出提示號.

如果不想每次下 ampy 指令都要指定 port, 可以先設定環境參數 AMPY_PORT, 例如 :

D:\test>SET AMPY_PORT=COM4       #設定環境參數 AMPY_PORT
D:\test>ampy ls                                          #不用再指定 --port 參數
boot.py
main.py
data.txt
hello.py

參考 :

Run Code
File Operations

下面是 Adafruit 關於檔案系統的教學影片 :

# Micropython esp8266 Tutorial 4 - Filesystem and ampy part 2



2. mpfshell :

此檔案系統管理套件的功能相當於 ampy 加上 webrepl, 但 mpfshell 在 ESP8266 板子不是透過串列埠來連線, 而是與 webrepl 一樣是透過 WiFi 網路連線 (使用 websocket).

mpfshell 本身須離線安裝, 可由下面網址下載 mpfshell 的 zip 檔 :

https://github.com/wendlers/mpfshell

由於 mpfshell 相依於 colorama 與 websocket-client, 必須先安裝這兩個套件才能安裝 mpfshell, 可以在線安裝 :

D:\test>pip3 install colorama
Collecting colorama
  Downloading colorama-0.3.9-py2.py3-none-any.whl
Installing collected packages: colorama
Successfully installed colorama-0.3.9

D:\test>pip3 install websocket_client
Collecting websocket_client
  Downloading websocket_client-0.40.0.tar.gz (196kB)
B 436kB/s
Requirement already satisfied: six in c:\python36\lib\site-packages (from websoc
ket_client)
Installing collected packages: websocket-client
  Running setup.py install for websocket-client ... done
Successfully installed websocket-client-0.40.0

也可以下載這兩個套件後離線安裝 :

https://pypi.python.org/pypi/colorama
https://pypi.python.org/pypi/websocket-client/

安裝程序如下 :

D:\test>pip3 install d:\python\colorama-0.3.9-py2.py3-none-any.whl
Processing d:\python\colorama-0.3.9-py2.py3-none-any.whl
Installing collected packages: colorama
Successfully installed colorama-0.3.9

D:\test>pip3 install d:\python\websocket_client-0.40.0.tar.gz
Processing d:\python\websocket_client-0.40.0.tar.gz
Requirement already satisfied: six in c:\python36\lib\site-packages (from websoc
ket-client==0.40.0)
Installing collected packages: websocket-client
  Running setup.py install for websocket-client ... done
Successfully installed websocket-client-0.40.0

最後安裝 mpfshell 本身 :

D:\test>pip3 install d:\python\mpfshell-master.zip
Processing d:\python\mpfshell-master.zip
Requirement already satisfied: pyserial in c:\python36\lib\site-packages (from m
pfshell==0.8.0)
Requirement already satisfied: colorama in c:\python36\lib\site-packages (from m
pfshell==0.8.0)
Requirement already satisfied: websocket_client in c:\python36\lib\site-packages
 (from mpfshell==0.8.0)
Requirement already satisfied: six in c:\python36\lib\site-packages (from websoc
ket_client-<mpfshell==0.8.0)
Installing collected packages: mpfshell
  Running setup.py install for mpfshell ... done
Successfully installed mpfshell-0.8.0

安裝完畢後開啟命令提示字元視窗, 輸入 mpfshell 卻無法執行 :

E:\test>mpfshell
'mpfshell' 不是內部或外部命令、可執行的程式或批次檔。

原因是安裝在 Python 的 Scripts 目錄下的 mpfshell 缺少副檔名 .py 之故, 只要手動在其後面加上 .py 即可 :


這樣再次輸入 mpfshell 即可進入 mpfs 介面了 :

D:\test>mpfshell

** Micropython File Shell v0.8.0, sw@kaltpost.de **
-- Running on Python 3.6 using PySerial 3.3 --

mpfs [/]>

按 Ctrl+C 會離開 mpfshell 介面回到命令提示字元.

由於 mpfshell 也是使用 webrepl 連線 ESP8266, 而 webrepl 無法多重連線, 因此若 WebREPL client 還在連線中的話, mpfshell 將連線失敗, 在 WebREPL client 畫面會顯示 :

"Concurrent WebREPL connection from(IP, port) rejected"


只要按 WebREPL client 上方的 "Disconnect" 鈕離線, mpfshell 就能順利連線了 :

D:\test>mpfshell                             #進入 mpfshell 介面

** Micropython File Shell v0.8.0, sw@kaltpost.de **
-- Running on Python 3.6 using PySerial 3.3 --

mpfs [/]> help                                  #指令列表

Documented commands (type help <topic>):
========================================
EOF  cd     exec  get   lcd  lpwd  md    mput  mrm   put  repl
cat  close  exit  help  lls  ls    mget  mpyc  open  pwd  rm

help 後面加上指令可以查看該指令說明, 例如 :

mpfs [/]> help put                            #顯示 put 指令之說明
put <LOCAL FILE> [<REMOTE FILE>]
        Upload local file. If the second parameter is given,
        its value is used for the remote file name. Otherwise the
        remote file will be named the same as the local file.

mpfs [/]> open ws:192.168.2.107    #連線 ESP8266
webrepl passwd:                                     #輸入 webrepl 密碼 123456

Failed to open: ws:192.168.2.107           #因 WebREPL client 連線中著導致 mpfshell 無法連線

mpfs [/]> open ws:192.168.2.107     #將 WebREPL client 離線後 mpfshell 即可順利連線
webrepl passwd:                                     #輸入 webrepl 密碼 123456
Connected to esp8266                             #連線成功
mpfs [/]> ls                                         #顯示檔案與目錄列表

Remote files in '/':

       boot.py
       main.py
       data.txt
       hello.py

mpfs [/]> cat boot.py                          #顯示檔案內容
#import esp
#esp.osdebug(None)
import gc
import webrepl
webrepl.start(password="123456")
gc.collect()

mpfs [/]> md log                                 #新增目錄 log
mpfs [/]> ls                                          #顯示檔案與目錄列表

Remote files in '/':

       boot.py
       main.py
       data.txt
       hello.py

mpfs [/]> cd log                                   #切換到 log 目錄

mpfs [/log]> pwd                                 #顯示目前路徑
/log

mpfs [/log]> help put                          #顯示 put 指令說明
put <LOCAL FILE> [<REMOTE FILE>]
        Upload local file. If the second parameter is given,
        its value is used for the remote file name. Otherwise the
        remote file will be named the same as the local file.

mpfs [/log]> put data.log                     #上傳 data.log 至 ESP8266        
mpfs [/log]> ls                                       #顯示檔案與目錄列表

Remote files in '/log':

 <dir> ..
       data.log                                                 #log 下有剛上傳的 data.log

mpfs [/log]> cat data.log                       #顯示檔案內容
Hello World!

mpfs [/log]> get data.log                       #下載 data.log

mpfs [/log]> rm data.log                       #刪除檔案
mpfs [/log]> cd ..                                    #切換目錄至上一層
mpfs [/]> rm log                                     #刪除目錄  log
mpfs [/]> ls                                             #顯示檔案與目錄列表

Remote files in '/':                                        #已無 log 目錄

       boot.py
       main.py
       data.txt
       hello.py

mpfshell 也有本地端檔案系統操作指令 (L 開頭的除了 ls 外都是本機指令), 不須跳出 mpfshell 也可以切換本機路徑或顯示檔案 :

mpfs [/]> lpwd                                        #顯示本機目前路徑
D:\test

mpfs [/]> lcd ..                                        #切換本機目前路徑至上一層

mpfs [/]> lpwd                                        #顯示本機目前路徑
D:\
mpfs [/]> lcd test                                    #切換本機目前路徑至 test 目錄
mpfs [/]> lpwd                                        #顯示本機目前路徑
D:\test

mpfs [/]> lls                                            #顯示本機目前路徑下之檔案與目錄

Local files:

       boot.py
       data.log
       hello.py
       menutest.java
       pyautogui_1.py
       pyautogui_2.py
       tk1.py

m 開頭的指令 mget, mput, 與 mrm 為使用正規表示法來整批下載, 上傳以及刪除遠端檔案系統中的檔案. 這在需要大量處理遠端檔案時非常有用, 例如下載 ESP8266 所記錄的 log 檔至本機中進一步處理, 或者整批下載後再整批刪除以節省 Flash 空間.

mpfs [/]> help mget      #顯示 mget 指令說明
mget <SELECTION REGEX>
        Download all remote files that match the given regular expression.
        The local files will be named the same as the remote files.

        "mget" does not get directories, and it is note recursive.


mpfs [/]> lls                   #顯示本機目前路徑下檔案目錄

Local files:                          #目前路徑下是空的


mpfs [/]> ls                     #顯示遠端目前路徑下檔案目錄

Remote files in '/':                #有 4 個檔案

       boot.py
       main.py
       data.txt
       hello.py

mpfs [/]> mget .*\.py      #以正規表達式整批下載 .py 檔
 * get boot.py
 * get main.py
 * get hello.py
mpfs [/]> lls

Local files:                            #整批下載了 3 個 .py 檔案

       boot.py
       hello.py
       main.py

mpfshell 的 repl 指令還可以像 Putty 連線那樣進入 REPL 介面 (透過 websocket 連線) :

mpfs [/]> repl
>
*** Exit REPL with Ctrl+Q ***

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>> print('Hello!')
Hello!
(按 Ctrl+Q)
mpfs [/]>

可見於 REPL 介面按 Ctrl+Q 又回到 mpfshell 介面了. 也就是說有了 mpfshell 後其實就不需要 Putty 了, 這是 mpfshell 比 ampy 以及 webrepl 好用的地方. 我唯一不滿意的是它的文字顏色.


3. rshell :

rshell 與 mpfshell 一樣都具有模擬 REPL 功能

在線安裝 :

E:\test>pip3 install rshell
Collecting rshell
  Downloading rshell-0.0.9.tar.gz
Requirement already satisfied: pyserial in c:\python36\lib\site-packages (from r
shell)
Collecting pyudev>=0.16 (from rshell)
  Downloading pyudev-0.21.0.tar.gz (89kB)
 561kB/s
Requirement already satisfied: six in c:\python36\lib\site-packages (from pyudev
>=0.16->rshell)
Installing collected packages: pyudev, rshell
  Running setup.py install for pyudev ... done
  Running setup.py install for rshell ... done
Successfully installed pyudev-0.21.0 rshell-0.0.9

可見 rshell 有兩個相依套件 pyserial 與 pyudev, 若要離線安裝須先下載安裝這兩個套件 :

https://pypi.python.org/pypi/pyudev
https://pypi.python.org/pypi/pyserial

先安裝 pyudev 套件與 pyserial :

C:\Users\Tony<pip3 install d:\python\pyudev-0.21.0.tar.gz
Processing d:\python\pyudev-0.21.0.tar.gz
Requirement already satisfied: six in c:\python36\lib\site-packages (from pyudev
==0.21.0)
Installing collected packages: pyudev
  Running setup.py install for pyudev ... done
Successfully installed pyudev-0.21.0

C:\Users\Tony>pip3 install d:\python\pyserial-3.3-py2.py3-none-any.whl
Requirement already satisfied: pyserial==3.3 from file:///E:/python/pyserial-3.3
-py2.py3-none-any.whl in c:\python36\lib\site-packages

然後下載 rshell 壓縮檔 :

https://github.com/dhylands/rshell

安裝 rshell :

C:\Users\Tony<pip3 install d:\python\rshell-master.zip
Processing d:\python\rshell-master.zip
Requirement already satisfied: pyserial in c:\python36\lib\site-packages (from r
shell==0.0.9)
Requirement already satisfied: pyudev<=0.16 in c:\python36\lib\site-packages (fr
om rshell==0.0.9)
Requirement already satisfied: six in c:\python36\lib\site-packages (from pyudev
<=0.16-<rshell==0.0.9)
Installing collected packages: rshell
  Running setup.py install for rshell ... done
Successfully installed rshell-0.0.9

但安裝完後輸入 rshell 卻出現錯誤 :

E:\test>rshell           #開啟 rshell
Traceback (most recent call last):
  File "C:\Python36\Scripts\rshell-script.py", line 11, in     load_entry_point('rshell==0.0.9', 'console_scripts', 'rshell')()
  File "c:\python36\lib\site-packages\pkg_resources\__init__.py", line 565, in l
oad_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "c:\python36\lib\site-packages\pkg_resources\__init__.py", line 2631, in
load_entry_point
    return ep.load()
  File "c:\python36\lib\site-packages\pkg_resources\__init__.py", line 2291, in
load
    return self.resolve()
  File "c:\python36\lib\site-packages\pkg_resources\__init__.py", line 2297, in
resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
  File "c:\python36\lib\site-packages\rshell\command_line.py", line 1, in e>
    import rshell.main
  File "c:\python36\lib\site-packages\rshell\main.py", line 65, in     import readline
ModuleNotFoundError: No module named 'readline'

雖然 rshell 功能似乎比 mpfshell 多, 但不知道是啥原因不能跑, 殘念 ~~~.

參考 :

Problems with running MicroPython on ESP8266 with 512K


2017-05-25 補充 :

最近在測試 MicroPython 時發現手上一堆舊版 ESP-01 因為只有 512K Flash 記憶體而用途受限, 看來只能當 Arduino 的 WiFi 模組了. 雖然還是可以灌 512K 韌體, 但因空間太小沒有檔案系統, 只能在 REPL 環境下執行 Python 程式, 沒辦法傳 boot.py 與 main.py 進去, 如果斷電重開機得重新在 REPL 輸入程式, 參考 :

Current _boot.py not compatible with ESP8266-01 #1986

"@doudz : Filesystem on modules with 512K flash is not supported (assuming yours is such). To help us diagnose it, move away _boot.py, build, boot, run "import port_diag", and provide its output."

2017-06-24 補充 :

今天在找 WDT 資料時從下列文章中看到應用程式配置的好方法 :

https://github.com/micropython/micropython/issues/2154

意思是不要把應用程式直接命名為 main.py, 而是取名為較容易辨識的 my_app.py, 然後在 main.py 裡面匯入 my_app, 再呼叫 my_app.main() 來執行應用程式, 例如 :


import my_app 
my_app.main()

參考 :

http://docs.micropython.org/en/latest/esp8266/esp8266/general.html#boot-process

這樣雖然要編輯上傳 main.py 與 my_app.py 兩個檔案, 比較麻煩, 但是在模組化而言, 一個應用可能會拆成好幾個 .py 程式, 這個方式會比較符合結構化原則. 

2017-06-26 補充 :


如果 ampy 突然無法上傳檔案, 且 REPL 中用 os.listdir() 發現有一堆 \x00, 這表示檔案系統已經局部毀損了, 但重灌韌體並無法解決檔案系統受損問題, 必須利用下列程式碼重建檔案系統 :

>>> from flashbdev import bdev
>>> uos.VfsFat.mkfs(bdev)
>>> vfs=uos.VfsFat(bdev)
>>> with open("/boot.py", "w") as f:
...     f.write("""\
...     import gc
...     gc.collect()
...     """)
...     f.close()
...

...

參考 :

MicroPython v1.9.1 版韌體測試