2019年7月1日 星期一

MicroPython on ESP32 學習筆記 (二) : 檔案系統

MicroPython 與 Arduino 的差異之一是它有檔案系統, 可以說是一個小型的作業系統, 應用程式只要上傳到檔案系統即可改變系統功能, 不需要燒錄韌體. MircoPython on ESP32 的檔案系統操作與 ESP8266 是一樣的, 有線方式可使用 ampy, mpfshell, 或 rshell 等介面, 無線方式則可用 WebREPL 來管理檔案系統, 參考 :

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

另外官方技術文件參考 :

https://docs.micropython.org/en/latest/esp32/quickref.html
https://docs.micropython.org/en/latest/library/index.html
# DFRobot MicroPython 教學文件 (Good)

有線部分我覺得 ampy 比較好用 (但需要將 ESP 開發板插入 PC 的 USB), 用法摘要整理如下 :


1. 安裝 adafruit-ampy 套件 :

在 Windows 下用 pip3 安裝 adafruit-ampy 套件 :

C:\Users\User>pip3 install adafruit-ampy 
Collecting adafruit-ampy
  Downloading https://files.pythonhosted.org/packages/59/99/f8635577c9a11962ec43714b3fc3d4583070e8f292789b4683979c4abfec/adafruit_ampy-1.0.7-py2.py3-none-any.whl
Collecting python-dotenv (from adafruit-ampy)
  Downloading https://files.pythonhosted.org/packages/57/c8/5b14d5cffe7bb06bedf9d66c4562bf90330d3d35e7f0266928c370d9dd6d/python_dotenv-0.10.3-py2.py3-none-any.whl
Requirement already satisfied: pyserial in c:\python37\lib\site-packages (from adafruit-ampy) (3.4)
Collecting click (from adafruit-ampy)
  Downloading https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl (81kB)
  Installing collected packages: python-dotenv, click, adafruit-ampy
Successfully installed adafruit-ampy-1.0.7 click-7.0 python-dotenv-0.10.3


2. 常用 ampy 指令 : 

安裝完畢後就可以用 ampy --help 查詢 ampy 的指令說明 :

D:\Python\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 environment variable.  [required]
  -b, --baud BAUD    Baud rate for the serial connection (default 115200).
                     Can optionally specify with AMPY_BAUD environment
                     variable.
  -d, --delay DELAY  Delay in seconds before entering RAW MODE (default 0).
                     Can optionally specify with AMPY_DELAY 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 board.
  reset  Perform soft reset/reboot of the board.
  rm     Remove a file from the board.
  rmdir  Forcefully remove a folder and all its children from the board.
  run    Run a script and print its output.

常用的指令列表如下 (以 COM8 為例) :


 指令 說明
 ampy --help 顯示 ampy 指令格式說明
 ampy --port COM8 ls 顯示 COM4 所連接之 ESP32 根目錄下的檔案與子目錄
 ampy --port COM8 mkdir lib 在根目錄下建立子目錄 lib
 ampy --port COM8 mkdir lib\lib0 在根目錄的子目錄 lib 下建立孫目錄 lib0
 ampy --port COM8 ls lib 顯示根目錄的子目錄 lib 下的檔案與孫目錄
 ampy --port COM8 rmdir lib 刪除根目錄下的子目錄 lib
 ampy --port COM8 get boot.py 顯示根目錄下的檔案 boot.py 內容
 ampy --port COM8 put main.py 上傳檔案 main.py 至根目錄下
 ampy --port COM8 put s.txt /tmp/s.txt 上傳檔案 s.txt 至 /tmp 目錄下
 ampy --port COM8 run main.py 執行根目錄下的 main.py
 ampy --port COM8 rm main.py 刪除根目錄下的檔案 main.py
 ampy --port COM8 reset 重啟 (reset) 系統


注意! 由於 ampy 指令會使用 COM port (連線到 ESP32 上的 UART 埠), 因此下 ampy 指令之前須關閉 PuTTY 連線 (除了 ampy --help 外), 否則會因為抓不到 COM 埠而出現如下錯誤 :

C:\Users\User>ampy --port COM8 ls 
Traceback (most recent call last):
  File "c:\python37\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "c:\python37\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "C:\Python37\Scripts\ampy.exe\__main__.py", line 9, in <module>
  File "c:\python37\lib\site-packages\click\core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "c:\python37\lib\site-packages\click\core.py", line 717, in main
    rv = self.invoke(ctx)
  File "c:\python37\lib\site-packages\click\core.py", line 1134, in invoke
    Command.invoke(self, ctx)
  File "c:\python37\lib\site-packages\click\core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "c:\python37\lib\site-packages\click\core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "c:\python37\lib\site-packages\ampy\cli.py", line 99, in cli
    _board = pyboard.Pyboard(port, baudrate=baud, rawdelay=delay)
  File "c:\python37\lib\site-packages\ampy\pyboard.py", line 147, in __init__
    raise PyboardError('failed to access ' + device)
ampy.pyboard.PyboardError: failed to access COM8 

不過即使 PuTTY 連線 ESP32 中用 ampy --help 也不會出現如上錯誤, 這是因為 ampy --help 只是 ampy 顯示本身資訊而已, 並未真正連線 ESP32 之故.


3. 修改 pyboard.py :

使用 ampy --help 查詢 ampy 的指令說明不會有問題, 但其他的指令卻可能會出現另一個錯誤訊息, 即使 PuTTY 關掉也是一樣. 例如用 ls 指令查詢根目錄下的內容時出現 "could not enter raw repl" 的錯誤訊息 :

D:\Python\test>ampy --port COM8 ls 
b'\x1b[0;32mI (439) cpu_start: Pro cpu up.\x1b[0m\r\n\x1b[0;32mI (439) cpu_start: Application information:\x1b[0m\r\n\x1b[0;32mI (439) cpu_start: Compile time:     Jun 25 2019 12:38:11\x1b[0m\r\n\x1b[0;32mI (443) cpu_start: ELF file SHA256:  0000000000000000...\x1b[0m\r\n\x1b[0;32mI (449) cpu_start: ESP-IDF:          v3.3-beta1-694-g6b3da6b18\x1b[0m\r\n\x1b[0;32mI (455) cpu_start: Starting app cpu, entry point is 0x40082b54\x1b[0m\r\n\x1b[0;32mI (0) cpu_start: App cpu up.\x1b[0m\r\n\x1b[0;32mI (466) heap_init: Initializing. RAM available for dynamic allocation:\x1b[0m\r\n\x1b[0;32mI (472) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM\x1b[0m\r\n\x1b[0;32mI (479) heap_init: At 3FFB9ED0 len 00026130 (152 KiB): DRAM\x1b[0m\r\n\x1b[0;32mI (485) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM\x1b[0m\r\n\x1b[0;32mI (491) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM\x1b[0m\r\n\x1b[0;32mI (498) heap_init: At 400921FC len 0000DE04 (55 KiB): IRAM\x1b[0m\r\n\x1b[0;32mI (504) cpu_start: Pro cpu start user code\x1b[0m\r\n\x1b[0;32mI (74) cpu_start: Starting scheduler on PRO CPU.\x1b[0m\r\n\x1b[0;32mI (0) cpu_start: Starting scheduler on APP CPU.\x1b[0m\r\n\x1b[0;32mI (30) modsocket: Initializing\x1b[0m\r\nI (50) wifi: wifi driver task: 3ffe2f08, prio:23, stack:3584, core=0\r\nI (184) wifi: wifi firmware version: 7240fb7\r\nI (184) wifi: config NVS flash: enabled\r\nI (194) wifi: config nano formating: disabled\r\n\x1b[0;32mI (194) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE\x1b[0m\r\n\x1b[0;32mI (204) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE\x1b[0m\r\nI (224) wifi: Init dynamic tx buffer num: 32\r\nI (234) wifi: Init data frame dynamic rx buffer num: 32\r\nI (234) wifi: Init management frame dynamic rx buffer num: 32\r\nI (234) wifi: Init management short buffer num: 32\r\nI (234) wifi: Init static rx buffer size: 1600\r\nI (244) wifi: Init static rx buffer num: 10\r\nI (244) wifi: Init dynamic rx buffer num: 32\r\nStarted webrepl in normal mode\r\nMicroPython v1.11-63-gd889def06 on 2019-06-25; ESP32 module with ESP32\r\nType "help()" for more information.\r\n>>> '
Traceback (most recent call last):
  File "c:\python37\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "c:\python37\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "C:\Python37\Scripts\ampy.exe\__main__.py", line 9, in <module>
  File "c:\python37\lib\site-packages\click\core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "c:\python37\lib\site-packages\click\core.py", line 717, in main
    rv = self.invoke(ctx)
  File "c:\python37\lib\site-packages\click\core.py", line 1137, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "c:\python37\lib\site-packages\click\core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "c:\python37\lib\site-packages\click\core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "c:\python37\lib\site-packages\ampy\cli.py", line 194, in ls
    for f in board_files.ls(directory, long_format=long_format, recursive=recursive):
  File "c:\python37\lib\site-packages\ampy\files.py", line 162, in ls
    self._pyboard.enter_raw_repl()
  File "c:\python37\lib\site-packages\ampy\pyboard.py", line 192, in enter_raw_repl
    raise PyboardError('could not enter raw repl')
ampy.pyboard.PyboardError: could not enter raw repl

我記得以前在 ESP8266 上使用 ampy 時不曾遇到此問題, 而且我在另一台 Win 10 電腦也沒有這個問題, why? 幸好找到下面這兩篇文章, 其中 "markserrano915" 提出的解決辦法經我測試確實可行, 參考 :

ESP8266 Micropython "Could not enter raw repl" #19
[ESP8266] MAC OS AMPY上傳、執行程式碼與無法進入RAW REPL

原來只要修改 ampy 安裝目錄下的 pyboard.py 這個檔案, 在 enter_raw_repl() 函數裡面添加一行 time.sleep(2) 即可. 我的 Python 安裝目錄為 C:\Python37, 而 ampy 是安裝在 Lib\site-packages 下, 因此在 C:\Python37\Lib\site-packages\ampy 下可找到 pyboard.py :





先備份原始的 pyboard.py, 然後用文字編輯器開啟此檔, 搜尋 'enter_raw_repl' 這個函數, 裡面有一個 while 迴圈, 其結束之後有一個空行, 在此空行添加 time.sleep(2), 然後存檔即可 :

    def enter_raw_repl(self):
        # Brief delay before sending RAW MODE char if requests
        if _rawdelay > 0:
            time.sleep(_rawdelay)

        self.serial.write(b'\r\x03\x03') # ctrl-C twice: interrupt any running program

        # flush input (without relying on serial.flushInput())
        n = self.serial.inWaiting()
        while n > 0:
            self.serial.read(n)
            n = self.serial.inWaiting()
        time.sleep(2)      #此行原為空行, 加入此指令 (參數不可小於 0.5)
        self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL




這樣就可以順利使用 ampy 了 :

D:\Python\test>ampy --port COM8 ls 
/boot.py
/webrepl_cfg.py

雖然不一定會遇到這問題, 但凡有此症頭可試試上面的解決辦法.


 4. 測試 ampy 指令 :

首先在根目錄下建一個子目錄 lib :

D:\Python\test>ampy --port COM8 mkdir lib 

用 ls 查詢果然出現了新的子目錄 :

D:\Python\test>ampy --port COM8 ls
/boot.py
/lib 
/webrepl_cfg.py

接著在 lib 下建立孫目錄 lib0 後用 ls 查詢 lib 內容 :

D:\Python\test>ampy --port COM8 mkdir lib/lib0   
D:\Python\test>ampy --port COM8 ls lib   
/lib/lib0   

注意, 在 Windows 下子目錄區隔符號也可以用倒斜線 \. 其次, 如果只查詢根目錄的話不會顯示所有更下層目錄, 必須指定 ls lib 才行. 移除上層目錄會將底下的所有檔案與目錄都刪除 :

D:\Python\test>ampy --port COM8 rmdir lib 

D:\Python\test>ampy --port COM8 ls 
/boot.py
/webrepl_cfg.py

可見 lib 與底下的 lib0 都不見了. 接著是用 get 讀取檔案內容, 以 boot.py 為例 :

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

然後編輯一個 main.py 檔案, 裡面只有 print('ok') 一個敘述, 然後用 put 指令上傳到 ESP32 的跟目錄下 :

D:\Python\test>ampy --port COM8 put main.py 

用 ls 顯示根目錄內容可知已上傳成功 :

D:\Python\test>ampy --port COM8 ls 
/boot.py
/main.py
/webrepl_cfg.py

然後用 run 指令執行 main.py :

D:\Python\test>ampy --port COM8 run main.py 
ok

結果正確顯示 'ok'.

如果是上傳到子目錄底下, 例如將 binascii.py 上傳到 lib 下面 :

D:\Python\test>ampy --port COM3 put binascii.py /lib/binascii.py 

用 rm 指令可刪除檔案 main.py :

D:\Python\test>ampy --port COM8 rm main.py 

用 ls 指令確定檔案已刪除 :

D:\Python\test>ampy --port COM8 ls 
/boot.py
/webrepl_cfg.py

最後用 reset 指令可執行軟開機重啟 :

D:\Python\test>ampy --port COM8 reset

至於無線的 WebREPL 部分參考之前 ESP8266 的測試, 方式是一樣的 :

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

特別注意 :

不要在 main.py 裡面寫無限迴圈, 否則你只能上傳一次 main.py, 因為之後每次 reset 板子都會自動進入無限迴圈佔據整個程序, 無法回應之後的 ampy 呼叫, ampy 指令全部均無反應, 只有重新燒錄韌體可解決.


5. 檔案處理 :

讀寫文字檔使用內建函數 open() 與 close() :

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

開啟模式有如下三種 :

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

open() 會傳回一個檔案參考物件, 其方法如下 :

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

讀取並顯示檔案內容有一個便捷的 with open() as 語法, 讀取結束會自動關檔, 不需呼叫 close() :

with open('test.txt', 'a') as lines:
    for line in lines:
        print(line)

Python 內建模組中支援檔案目錄管理的模組有 os, os.path, shutil, glob.glob, os.walk 等, 參考 :

Python 學習筆記 : 檔案處理

但 MicroPython 只是一個 CPython 的子集, 只實作了 os 模組的主要方法, 並沒有全部實作這些功能 :

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

os 模組只實作了下列方法 :

 os 模組方法 說明
 listdir(dir) 顯示目錄 dir 內容, 例如 '.', '..' 或 'lib'
 mkdir(dir) 新增目錄 dir, 例如 'lib'
 rmdir(dir) 刪除目錄 dir, 例如 'lib' 或 '../lib'
 chdir(dir) 切換至目錄 dir, 例如 'lib' 或 '../lib'
 getcwd() 顯示目前工作目錄
 rename(old, new) 更改檔名或目錄名稱 old 為 new
 remove(file) 刪除檔案 file

例如 :

>>> import os 
>>> dir(os) 
['__class__', '__name__', 'remove', 'VfsFat', 'chdir', 'dupterm', 'dupterm_notify', 'getcwd', 'ilistdir', 'listdir', 'mkdir', 'mount', 'rename', 'rmdir', 'stat', 'statvfs', 'umount', 'uname', 'urandom']
>>> os.listdir('.')   
['boot.py', 'webrepl_cfg.py', 'main.py']     
>>> os.mkdir('tmp') 
>>> os.listdir('.') 
['boot.py', 'webrepl_cfg.py', 'main.py', 'tmp'] 
>>> os.chdir('tmp') 
>>> os.getcwd() 
'/tmp'
>>> f=open('test.txt', 'w') 
>>> f.write('Hello\n') 
6
>>> f.write('World') 
5
>>> f.close()                  #必須關檔才能讀取
>>> os.listdir('.') 
['test.txt']
>>> os.rename('test.txt','tmp.txt') 
>>> os.listdir('.') 
['tmp.txt']
>>> with open('tmp.txt', 'r') as lines:   
...     for line in lines: 
...         print(line) 
...
...
...
Hello

World
>>> os.remove('tmp.txt') 
>>> os.listdir('.')   
[]
>>> os.chdir('..') 
>>> os.getcwd()
'/'
>>> os.rmdir('tmp') 
>>> os.listdir('.') 
['boot.py', 'webrepl_cfg.py', 'main.py']

值得一提的是, os 模組裡面沒有 move 指令, 要將某個檔案移到另一個目錄可以用 rename() 來完成, 例如要將目前目錄下的 test.txt 移到子目錄 temp 下面可以這麼做 :

>>> os.rename('test.txt','temp/test.txt') 

特別是在使用 WebREPL 介面時, 它只能將檔案上傳到 ESP32 的根目錄下, 就可以用 rename() 將其移至其他目錄.

參考 :

https://docs.micropython.org/en/latest/esp32/tutorial/intro.html
https://nick.zoic.org/talk/lca2017/
BugWorkShop - 甲蟲工作室
MicroPython on ESP32
MicroPython Programming with ESP32 and ESP8266 eBook

沒有留言 :