「アバター」はMicroPythonプログラミング基礎の宝庫だった

以下の記事を大いに参考にさせていただきました。感謝いたします。

[1] https://github.com/h-akanuma/M5StackAvatarPython
[2] https://qiita.com/infinite1oop/items/7e34f9eb16ae274c9de8
[3] https://github.com/meganetaaan/m5stack-avatar/blob/master/README_ja.md

もともと [1] が動いてくれず，なぜだろうと調べたのと並行して，自分でも作ってみたら，Python初学者の私には良い教材だったというお話です。

![PXL_20230104_150831905_TS_AdobeExpress (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533274/8d32a4b3-c54b-6a2f-a1e2-46748f09a589.gif)

[3] はかなり本格的ですが，私はMicroPythonでできることにこだわりたい。私自身が他のことに転用するのに都合がいいように，自分で作ってみることにしました。

アバターというわりには簡素なので，以下 顔 または face と書いています。

# 目指したこと

見た目のかわいさは完全に [1] を模倣させて頂きました。繰り返しますが本当に感謝です。
以下の点を変更してみました。

- 顔の位置，大きさを変えられるようにする。大きな画面を利用して，他のエリアで他の情報を表示できるようにするため。
- `speak`にて，[1]ではスクロールで実装されているが，「状態表示」をするには消えないほうが便利と考え，普通に表示するようにした（手抜きともいう）

# 個人的に勉強になったこと

`_thread` というのを使うのは初めてでした。MicroPythonのバージョンがあがり，この`_thread`の仕様が変わっていたことが [1] が素直に動かない原因でした。

[1]をみると，目を動かす等のときにスレッドを使っていました。なるほど，目を動かしている間にも他の作業ができるようにしているのだな，と理解できました。私も顔の表情づくりで時間がかかりそうな箇所は，スレッドで実装しました。

- 新しいスレッドの開始例

```main（抜粋）.py
    def _blinkEyes(self, t):
        if t==0:
            t=self.exTime
        else:
            t=t*1000
        self.animationStatus['eye']=True
        self.eyeOpenLeft.hide()
        self.eyeOpenRight.hide()
        self.eyeCloseLeft.show()
        self.eyeCloseRight.show()
        wait_ms(t)
        self.eyeCloseLeft.hide()
        self.eyeCloseRight.hide()
        self.eyeOpenLeft.show()
        self.eyeOpenRight.show()
        wait_ms(t)
        del self.animationStatus['eye']
        _thread.exit()

    def blinkEyes(self, t=0):
        """close eyes t[sec] and then open. t=0 means default time"""
        while ('eye' in self.animationStatus):
            wait_ms(10)
        _thread.start_new_thread(self._blinkEyes, (t,))
        wait_ms(10)
```

ここで `_thread.start_new_thread(self._blinkEyes, (t,))` の部分につまずきました。従来必要だったスレッドのキーワードが不要となりました。
また，スレッドの関数への引数はtupleでなくてはなりませんでした。上記では `(t,)` として１個の引数を渡しました。

[1]では `_thread.sendmsg` および `_thread.getmsg()` でスレッドとのメッセージやり取りができたようなのですが，この関数がなくなってしまいましたので，上記では dictionary である `self.animationStatus['eye']` を使いました。

また，スレッドを多重に作成しないように（作成するとエラーとなる），時間待ちを置く場所を模索しました。

classの書き方を，これを機会に少しだけですが調べました。更に追加すれば顔を動かしたり，複数個持てたりできそうですが，ここでは手を抜きました。

[1]では顔のパーツは低レベル描画関数で描かれていて，消したいときは背景色にする，という手法でした。感嘆符と「青ざめ」マークはそのまま移植しましたが，のこりはM5のclassで実装しました。

以下長いソースで失礼

```myFace.py
from m5stack import *
from m5ui import *
from uiflow import *
import time
import _thread

# display 320*240

class face:
    def __init__(self, x=0, y=0, scale=1):

        self.fgColor=lcd.WHITE
        self.bgColor=0x01082f
        self.exTime = 200 # blink, lipping time [msec]

        self.top_x=int(x)
        self.top_y=int(y)
        self.scale=scale

        setScreenColor(self.bgColor)

        self.eyeCloseLeft = M5Rect(self._scaleX(65), self._scaleY(67),
                      self._scaleV(50), self._scaleV(6), self.fgColor, self.fgColor)
        self.eyeCloseRight = M5Rect(self._scaleX(205), self._scaleY(67),
                       self._scaleV(50), self._scaleV(6), self.fgColor, self.fgColor)
        self.eyeCloseLeft.hide()
        self.eyeCloseRight.hide()
        self.eyeOpenLeft = M5Circle(self._scaleX(90), self._scaleY(70),
                       self._scaleV(10), self.fgColor, self.fgColor)
        self.eyeOpenRight = M5Circle(self._scaleX(230), self._scaleY(70),
                        self._scaleV(10), self.fgColor, self.fgColor)
        self.lipOpen = M5Rect(self._scaleX(145), self._scaleY(128),
               self._scaleV(30), self._scaleV(30), self.fgColor, self.fgColor)

        self.lipOpen.hide()
        self.lipClose = M5Rect(self._scaleX(130), self._scaleY(140),
               self._scaleV(60), self._scaleV(6), self.fgColor, self.fgColor)

        self.labelMessage = M5TextBox(5, 205, '', lcd.FONT_DejaVu24, self.fgColor, rotate=0)

        self.animationStatus={} # for example, during winking, self.animationStatus['eye'] is True

    def _scaleX(self, x):
        return self.top_x+int(x*self.scale)

    def _scaleY(self, y):
        return self.top_y+int(y*self.scale)

    def _scaleV(self, v):
        return max(int(v*self.scale),1)

    def _blinkEyes(self, t):
        if t==0:
            t=self.exTime
        else:
            t=t*1000
        self.animationStatus['eye']=True
        self.eyeOpenLeft.hide()
        self.eyeOpenRight.hide()
        self.eyeCloseLeft.show()
        self.eyeCloseRight.show()
        wait_ms(t)
        self.eyeCloseLeft.hide()
        self.eyeCloseRight.hide()
        self.eyeOpenLeft.show()
        self.eyeOpenRight.show()
        wait_ms(t)
        del self.animationStatus['eye']
        _thread.exit()

    def blinkEyes(self, t=0):
        """close eyes t[sec] and then open. t=0 means default time"""
        while ('eye' in self.animationStatus):
            wait_ms(10)
        _thread.start_new_thread(self._blinkEyes, (t,))
        wait_ms(10)

    def _pale_color(self, c): # from https://github.com/h-akanuma/M5StackAvatarPython
        lcd.rect(self._scaleX(200), self._scaleY(0), self._scaleV(5), self._scaleV(40), c, c)
        lcd.rect(self._scaleX(220), self._scaleY(0), self._scaleV(5), self._scaleV(45), c, c)
        lcd.rect(self._scaleX(240), self._scaleY(0), self._scaleV(5), self._scaleV(50), c, c)
        lcd.rect(self._scaleX(260), self._scaleY(0), self._scaleV(5), self._scaleV(55), c, c)
        lcd.rect(self._scaleX(40), self._scaleY(100), self._scaleV(5), self._scaleV(40), c, c)
        lcd.rect(self._scaleX(60), self._scaleY(103), self._scaleV(5), self._scaleV(35), c, c)
        lcd.rect(self._scaleX(80), self._scaleY(106), self._scaleV(5), self._scaleV(30), c, c)
        lcd.rect(self._scaleX(100), self._scaleY(109), self._scaleV(5), self._scaleV(25), c, c)

    def paleOn(self):
        self._pale_color(lcd.BLUE)

    def paleOff(self):
        self._pale_color(self.bgColor)

    def _exclamation_color(self, c): # from https://github.com/h-akanuma/M5StackAvatarPython
        lcd.rect(self._scaleX(280), self._scaleY(20), self._scaleV(10), self._scaleV(30), c, c)
        lcd.rect(self._scaleX(280), self._scaleY(20+30+8), self._scaleV(10), self._scaleV(10), c, c)

    def exclamationOn(self):
        self._exclamation_color(lcd.RED)

    def exclamationOff(self):
        self._exclamation_color(self.bgColor)


    def _openLip(self):
        self.animationStatus['lip']=True
        self.lipClose.hide()
        self.lipOpen.show()
        wait_ms(self.exTime)
        self.lipOpen.hide()
        self.lipClose.show()
        wait_ms(self.exTime)
        del self.animationStatus['lip']
        _thread.exit()


    def openLip(self):
        """open lip and then close"""
        while('lip' in self.animationStatus):
            wait_ms(10)
        _thread.start_new_thread(self._openLip, ())
        wait_ms(10)

    def speakText(self, s):
        self.labelMessage.setText(s)
        for _ in range(max(int(len(s)/3),1)):
            self.openLip()

# end of class

a=face(x=320/2, y=0, scale=0.5)
#a=face()

wait(2)
a.openLip()

wait(2)
a.speakText('Welcome!')

wait(2)
a.speakText('What?')
a.exclamationOn()
a.blinkEyes(1)
wait(2)
a.exclamationOff()

a.paleOn()
a.speakText('Sad...')
a.blinkEyes(1)
wait(2)
a.paleOff()

a.speakText('Bye...')
a.blinkEyes()
a.blinkEyes()
```