MicroPython的午睡(7) M5StickV, MicroPython再々復活

Joseph Halfmoon

最近のmakersなのかSTEMなのかの盛り上がりの余波で、今だRaspberry Pi PICOもmicro:bit V2も買えておりませぬ。そのためmicro:bit V1.5のBTネタへの移動にともない、本MicroPythonシリーズはM5StickVに担当してもらうことになりました。春の人事異動か?

※「MicroPython的午睡」投稿順 Indexはこちら

(今回使用のスクリプト全文は末尾に)

M5StickVはM5Stack社製の「AIカメラ」であります。小さな筐体、お手頃価格。しかし、その中核のKendryte K210 SoCは、デュアルコアのRISC-Vを頂き、倍精度浮動小数点演算対応、そしてKPUと呼ばれるNeural Network ProcessorやハードウエアFFTまで搭載しています。今までMicroPythonを走らせていた micro:bit に比べると性能的には段違い。過去、何度か「M5StickVネタ」を取り上げさせていただいておりましたが、当然な感じでAIがらみであったのです。しかし今回からしばらくは

MicroPythonネタ、とりあえずAIでない

に絞って行いたいと考えております。同じMicroPythonといいつつ、ハードウエアの違いから micro:bit上の実装とK210での実装はいろいろ違いがあるので。

また、M5StickVのMicroPython処理系である MaixPy を使用する場合、

MaixPy IDE

なるPC上のIDEが使用できるのです。このIDEは、AI向けの画処理に適したもので、リアルタイムでM5StickVのカメラが撮影した画像を解析しながら表示できたりする機能を持ちます。なかなか良いものだと思います。しかし今回は「他のMicroPython処理系」との互換性なども考えて、M5StickV上のMicroPythonのREPLをシリアル端末から直接制御する方法でやりたいと思います。

ファームウエアの更新

M5StickVの再々起動(数えてみたら購入以来3回目でした)にあたり、まずは搭載のファームウエア(MicroPythonの実装の一種であるMaixPy)を再々更新し、最新版にアップデートすることを考えました。ファームウエアの書き込み方法は、以下のページにあります。

M5StickV Maixpy Quick Start

方法としては以下の2つあります。

  1. EasyLoader、Windows版のみ、ハードを接続してボタンを押すだけ。ただし書きこめるファームウエアのバージョンは限定
  2. Kflash(Kflash_GUI)、Windows/Mac/Linux皆あり。ファームウエアバイナリを直接指定して書きこみ可能

実は1をやってバージョンが古かった(0.5.0 日付2020/7/21)ので、更新作業を行った日の最新版(0.6.2の34版 日付2021/3/16)のバイナリをダウンロード(前にも書きましたがモジュール構成でいろいろ選択肢あり。今回はM5StickVとファイル名にあるもの)して2の方法も行いました。しかし、動くのですが挙動が怪しい。それで結局1の安定版?に戻しました。なお、バイナリファイルは数日に1回くらいのペースで更新されつづけているようです。

バイナリファイルの在りか

REPLでの「開発」支援用のboot.py作成

さて今回REPL中心での「開発」するにあたって作成したのは

boot.py

なるファイルです(末尾に全ソースあり。)MaixPyは、RESET後、microSDカードが挿入されていればカードを自動マウント(/sdという名がマウントポイント)してくれるので、 /sdディレクトリの直下においておくと、自動実行されるファイルです。REPLからは、「CTRL-D」でソフトRESETがかかるので、キーボード一発で起動が可能です。

なお、MicroPython処理系によってはboot.pyが走った後、main.pyが起動されるものもあるのですが、MaixPyのこのバージョンではmain.pyが自動起動されないようです。ターミナルエミュレータでM5StickVに接続した状態でソフトRESETするとこんな感じ。

StartUSHELL

REPLでなく、何かプロンプトが返ってきますが。これが今回「開発」用にしつらえた(過去のファイルの寄せ集めですが)一種のシェルです。REPLからだと、SDカード内のMicroPythonスクリプトを実行するとき、

with open("xxx.py") as f:
    exec(f.read())

のようなことを書かないとならないですが、それを、

run xxx.py

などという簡単な形でやりたかった、というだけです。例えば、sdカードの中のファイルやディレクトリは、dirコマンドでリストできます(以前の回で作ってあったものの流用)

uShellDIR勿論、ディレクトリの移動も可能。ちゃんとプロンプト$ の左側に現在ディレクトリが表示されます。こんな感じ。なお、.. とかまだ実装していないので、移動するときは文字列でディレクトリ名を与えないとならないですが。

MaixPyは、pye()エディタを内蔵しているので、

edit xxx.py

などとすればファイルの編集が可能(pyeを呼び出すだけ)。テキストファイルの表示は type コマンドで以下のようにできます。

スクリプトの実行はRunコマンドにファイル名を渡せばOK。

uShellRUN

任意のファイルを実行できます。スクリプトから戻るか、CTRL-Cで割り込めばREPLのプロンプトに落ちてきます。先ほど述べたとおり、REPLでCTRL-Dするとboot.pyが再び走り先ほどの$プロンプトがでます。REPLでデバッグしながら「開発」できる?

今のところ使用できるコマンドは以下のとおり。

uShellHELP

末尾の led コマンドを打つと、ledとスイッチのデモが走ります。正面のAボタンを押すと背面のLED(結構明るい)の色が変化します。側面のBボタンで $プロンプトに戻ります。このコマンドはハードウエアを操作するコマンド群を追加する予定の原型であります。

この「Shellモドキ」は、SerialポートへのCUIですが、微妙にGUI指向もあり、M5StickVの画面にタイトルやステータスを表示もします(アイキャッチ画像ごらんください。)後でいろいろ使用の予定。M5StackにおけるM5ez的な?を指向。

LEDtestMicroShellまあ、これで、端末エミュレータからの操作は一通りできるようになったので、M5StickV上のMicroPythonを動かしていってみたいと思います。

MicroPython的午睡(6) micro:bit、ファイルシステムとメモリダンプ に戻る

MicroPython的午睡(8) M5StickV、ulab行列積、timeitデコレータ へ進む

import uos
import time
import lcd
from Maix import GPIO
from fpioa_manager import fm
from board import board_info

class ucmd:
    """micro cmd shell for M5StickV/MaixPy

    v0.1
    """
    buttonA = None
    buttonB = None
    buttonApressed = False
    buttonBpressed = False
    buttonAFlag = False
    buttonBFlag = False
    LedW = None
    LedR = None
    LedG = None
    LedB = None

    @classmethod
    def initKey(cls):
        fm.register(board_info.BUTTON_A, fm.fpioa.GPIO1)
        cls.buttonA = GPIO(GPIO.GPIO1, GPIO.IN, GPIO.PULL_UP)
        fm.register(board_info.BUTTON_B, fm.fpioa.GPIO2)
        cls.buttonB = GPIO(GPIO.GPIO2, GPIO.IN, GPIO.PULL_UP)

    @classmethod
    def initLed(cls):
        fm.register(board_info.LED_W, fm.fpioa.GPIO3)
        cls.LedW = GPIO(GPIO.GPIO3, GPIO.OUT)
        cls.LedW.value(1)
        fm.register(board_info.LED_R, fm.fpioa.GPIO4)
        cls.LedR = GPIO(GPIO.GPIO4, GPIO.OUT)
        cls.LedR.value(1)
        fm.register(board_info.LED_G, fm.fpioa.GPIO5)
        cls.LedG = GPIO(GPIO.GPIO5, GPIO.OUT)
        cls.LedG.value(1)
        fm.register(board_info.LED_B, fm.fpioa.GPIO6)
        cls.LedB = GPIO(GPIO.GPIO6, GPIO.OUT)
        cls.LedB.value(1)

    @classmethod
    def setLed(cls, b4):
        cls.LedW.value( (b4 & 0x8) >> 3 )
        cls.LedR.value( (b4 & 0x4) >> 2 )
        cls.LedG.value( (b4 & 0x2) >> 1 )
        cls.LedB.value( (b4 & 0x1) )

    @classmethod
    def updateKey(cls):
        if (cls.buttonA.value() == 0) and (not cls.buttonApressed):
            cls.buttonApressed = True
        if (cls.buttonA.value() == 1) and cls.buttonApressed:
            cls.buttonApressed = False
            cls.buttonAFlag = True
        if (cls.buttonB.value() == 0) and (not cls.buttonBpressed):
            cls.buttonBpressed = True
        if (cls.buttonB.value() == 1) and cls.buttonBpressed:
            cls.buttonBpressed = False
            cls.buttonBFlag = True

    @classmethod
    def wasButtonApressed(cls):
        if cls.buttonAFlag:
            cls.buttonAFlag = False
            return True
        return False

    @classmethod
    def wasButtonBpressed(cls):
        if cls.buttonBFlag:
            cls.buttonBFlag = False
            return True
        return False

    @classmethod
    def testLED(cls):
        cls.showStatus(" LED TEST running ")
        cls.initKey()
        cls.initLed()
        led = 15
        while(True):
            cls.updateKey()
            if cls.wasButtonBpressed():
                cls.setLed(0xF)
                cls.showStatus(" Ready")
                break
            if cls.wasButtonApressed():
                led = (led - 1) if led > 0 else 15
                cls.setLed(led)
            time.sleep_ms(100)

    @classmethod
    def cd(cls, pth):
        uos.chdir(pth)
        print('cd : ', uos.getcwd())

    @classmethod
    def dir(cls, pth):
        for item in uos.ilistdir(pth):
            if len(item) < 2:
                print(str(item))
            if (item[1] & 0x8000) != 0:
                print("F ",item[0],item[3])
            elif (item[1] & 0x4000) != 0:
                print("D ",item[0])
            else:
                print(str(item))

    @classmethod
    def run(cls, pth):
        exec(open(pth).read())

    @classmethod
    def type(cls, pth):
        with open(pth) as f:
            for lin in f:
                print(lin,end='')

    @classmethod
    def edit(cls, pth):
        pye(pth)

    @classmethod
    def help(cls):
        print("help       ... show this help.")
        print("exit/quit  ... return to REPL.")
        print("dir [pth]  ... list file and directories.")
        print("type fname ... show text file contents.")
        print("run fname  ... exec .py file.")
        print("ed fname   ... pye editor.")
        print("cd pth     ... change directory.")
        print("led        ... do LED/KEY demo.")

    @classmethod
    def cmd(cls):
        flag = True
        while (flag):
            print("{0} $ ".format(uos.getcwd()),end='')
            lin = input()
            linL = lin.split()
            if len(linL) < 1:
                continue
            elif len(linL) == 1:
                linL.append(uos.getcwd())
            try:
                if   linL[0].startswith('exit'):
                    flag = False
                elif linL[0].startswith('di'):
                    cls.dir(linL[1])
                elif linL[0].startswith('ty'):
                    cls.type(linL[1])
                elif linL[0].startswith('run'):
                    cls.run(linL[1])
                elif linL[0].startswith('ed'):
                    cls.edit(linL[1])
                elif linL[0].startswith('cd'):
                    cls.cd(linL[1])
                elif linL[0].startswith('led'):
                    cls.testLED()
                elif linL[0].startswith('help'):
                    cls.help()
                elif linL[0].startswith('q'):
                    break
            except:
                print("ERROR: ", str(linL))

    @classmethod
    def showMessage(cls, lin, arg):
        lin = 1 if lin < 1 else 6 if lin > 6 else lin
        lcd.draw_string(0, lin*16, arg, lcd.WHITE, lcd.BLACK)

    @classmethod
    def fillLine(cls, arg):
        nC = len(arg)
        if nC > 30:
            return arg[0:30]
        else:
            filling = " " * (30-nC)
        return arg + filling

    @classmethod
    def showStatus(cls, arg):
        lcd.draw_string(0,112,cls.fillLine(arg), lcd.BLACK, lcd.CYAN)

    @classmethod
    def showTitle(cls, arg):
        lcd.draw_string(0,0,cls.fillLine(arg), lcd.WHITE, lcd.BLUE)

def startLCD():
    lcd.init()
    lcd.rotation(0)

def main():
    print("Start uSHELL(in boot.py):")
    startLCD()
    ucmd.showTitle(" M5StickV uSHELL v0.1")
    ucmd.showStatus(" Ready ")
    ucmd.cmd()

if __name__ == "__main__":
    main()