M5Stack をシリアル接続してデバッグする(MicroPython)

 最近は M5Stack を主に m5cloud を使って MicroPython で色々と試しています。 m5cloud での開発は、 M5Stack が Wi-Fi に繋がっていればPCと直接接続しなくても良いというのもメリットの一つだと思うのですが、その分デバッグはしづらいところがあります。ちょっとした確認であれば lcd.print() 等で画面に出力して確認することもできるのですが、 SyntaxError などでそもそもちゃんと動かない時には起動時に下記のような画面で止まってしまい、原因が何なのかがわかりません。

f:id:akanuma-hiroaki:20181103120541j:plain

 なのでやはりデバッグするにはシリアルケーブルで接続して、出力を確認しながらするのが効率的です。

シリアル接続

 私の Mac 環境では M5Stack を USB ケーブルで接続すると、下記のようなデバイスとして認識されます。

$ ls -l /dev/tty.SLAB_USBtoUART 
crw-rw-rw-  1 root  wheel   21,  24 Nov  2 08:27 /dev/tty.SLAB_USBtoUART

 screen コマンドでデバイスに接続します。ボーレートは 115,200 を指定します。

$ screen /dev/tty.SLAB_USBtoUART 115200

シリアル出力確認

 シリアル接続した状態で M5Stack を再起動すると、下記のようにシリアルコンソールに情報が出力されます。

[M5Cloud] Downloading:/flash/main.py  ......
[M5Cloud] Downloading:/flash/README.md  .
ets Jun  8 2016 00:22:57

rst:0xc (SW_CPU_RESET),boot:0x17 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0018,len:4
load:0x3fff001c,len:4636
load:0x40078000,len:0
load:0x40078000,len:12948
entry 0x4007852c

Internal FS (SPIFFS): Mounted on partition 'internalfs' [size: 2424832; Flash address: 0x1B0000]
----------------
Filesystem size: 2221568 B
           Used: 56320 B
           Free: 2165248 B
----------------

Device ID:807d3ac471bc
Connect WiFi: SSID:XXXXXXXXXXXXXXXX PASSWD:XXXXXXXX network...
....
Connected. Network config: ('172.20.10.12', '255.255.255.240', '172.20.10.1', '172.20.10.1')
[M5-807d3ac471bc] M5Cloud connected.

FreeRTOS running on BOTH CORES, MicroPython task started on App Core (1).

 Reset reason: Soft CPU reset
    uPY stack: 19456 bytes
     uPY heap: 80000/19280/60720 bytes

MicroPython ESP32_LoBo_v3.2.16 - 2018-05-15 on M5Stack with ESP32
Type "help()" for more information.
>>>

 上記の例はエラーなく正常に起動できたケースですが、例えば SyntaxError があると上記の出力の途中に下記のようにエラーメッセージが出力されます。これでエラーの原因や場所が特定できます。

Traceback (most recent call last):
  File "main.py", line 11
SyntaxError: invalid syntax

 また、print デバッグを行いたいときは、 print() を使用すればその引数がシリアルコンソールに出力されます。例えば下記のように API のレスポンスの内容を print() で出力するようにしてみます。

response      = urequests.get(self.base_url.format(prefecture, self.api_key))
json          = response.json()
main          = json['main']
print(main)

 これを実行すると下記のように API のレスポンスがシリアルコンソールに出力されて行きます。

{'pressure': 1025, 'humidity': 54, 'temp_min': 283.15, 'temp_max': 287.15, 'temp': 284.75}
{'pressure': 1027, 'humidity': 74, 'temp_min': 283.15, 'temp_max': 287.15, 'temp': 284.81}
{'pressure': 1025, 'humidity': 54, 'temp_min': 288.15, 'temp_max': 288.15, 'temp': 288.15}
{'pressure': 1025, 'humidity': 54, 'temp_min': 283.15, 'temp_max': 287.15, 'temp': 284.75}
{'pressure': 1027, 'humidity': 74, 'temp_min': 283.15, 'temp_max': 287.15, 'temp': 284.81}

MicroPython のインタラクティブ実行

 正しく起動していれば、起動時の出力が終わると下記のように MicroPython のインタラクティブシェルが起動しますので、ここでインタラクティブの MicroPython を実行して動作を確認することができます。

MicroPython ESP32_LoBo_v3.2.16 - 2018-05-15 on M5Stack with ESP32
Type "help()" for more information.
>>> import time
>>> time.gmtime()
(1970, 1, 1, 0, 22, 50, 5, 1)
>>> 

 Python にはあっても MicroPython にはないクラスやメソッドもありますので、インタラクティブシェルで実際のボード上での挙動が確認できるのは便利ですね。

まとめ

 直接ケーブルで Mac と接続する必要はあるものの、上記のような情報なしで開発していくのは非効率なので、開発中はシリアルコンソールの利用が必須かと思います。今のところは m5cloud で開発をしていますが、バージョン管理等ができない不便さはあるので、開発もローカルで行ってシリアル接続で転送するやり方も検討してみようかと思います。

M5Stack Gray の MPU9250 の値を MicroPython で読み取る

 M5Stack Gray には MPU9250 が搭載されていて、加速度、ジャイロ、磁気を計測することができますが、買ってすぐに Arduino のサンプルスケッチを動かしてみただけだったので、 MicroPython で値を読み出してみました。

www.switch-science.com

MPU9250 モジュール

 M5Stack の MicroPython では MPU9250 モジュールが組み込まれているので、 import するだけで使えるようになっています。モジュールの内容は下記で参照することができます。

github.com

 MPU9250 は MPU6500(加速度、ジャイロ)+ AK8963(磁気) の組み合わせになっているようで、それぞれを明示的に初期化して MPU9250 の初期化時に指定することもできるようになっています。今回は磁気センサーの値から方位を計算することをしてみようとしたのですが、 AK8963 のキャリブレーションについてはメソッドが用意されているわけでもなさそうで、方法が調べ切れなかったので、ひとまず値をそのまま読み出すところまでにしています。

サンプルコード

 今回実装したサンプルは下記のような内容になります。 MPU9250 は I2C 接続になっていて、 SDA が 21番ピン、 SCL が 22 番ピンになっているようなので、 I2C のインスタンス作成時にそのピンを指定し、 MPU9250 のインスタンス作成時に I2C のインスタンスを渡しています。あとはそのインスタンスから acceleration gyro magnetic でそれぞれの値がタプルで返ってくるので、それをバラして画面表示させています。

from m5stack import lcd
from machine import I2C, Pin
from mpu9250 import MPU9250
import time

i2c = I2C(sda = 21, scl = 22)
sensor = MPU9250(i2c)

lcd.clear()
lcd.setCursor(0, 0)
lcd.setColor(lcd.WHITE)

fw, fh = lcd.fontSize()

lcd.print('Acceleration', 0, 0)
lcd.print('Gyro', 0, fh * 4)
lcd.print('Magnetic', 0, fh * 8)

while True:
    ax, ay, az = sensor.acceleration
    gx, gy, gz = sensor.gyro
    mx, my, mz = sensor.magnetic
    
    lcd.print(' ax: {:+.5f}'.format(ax), 0, fh * 1)
    lcd.print(' ay: {:+.5f}'.format(ay), 0, fh * 2)
    lcd.print(' az: {:+.5f}'.format(az), 0, fh * 3)
    
    lcd.print(' gx: {:+.5f}'.format(gx), 0, fh * 5)
    lcd.print(' gy: {:+.5f}'.format(gy), 0, fh * 6)
    lcd.print(' gz: {:+.5f}'.format(gz), 0, fh * 7)
    
    lcd.print(' mx: {:+.5f}'.format(mx), 0, fh * 9)
    lcd.print(' my: {:+.5f}'.format(my), 0, fh * 10)
    lcd.print(' mz: {:+.5f}'.format(mz), 0, fh * 11)
    
    time.sleep_ms(20)

 これを実行すると下記のような表示になります。

f:id:akanuma-hiroaki:20181027233113j:plain

 公式のサンプルも下記に公開されています。

https://github.com/m5stack/M5Cloud/blob/master/examples/mpu9250/basic/main.py

まとめ

 今回はとりあえずセンサーの値をそのまま読み出すだけでしたが、これらのセンサーの値はこのままでは意味がなく、計算して方位などがわかるようにしたかったのですが、そのためにはキャリブレーションが必要になってくるので、今後その方法も調べて、アバター等と組み合わせて有効に使えるようにしていきたいと思います。

M5Stack UI Flow で画像表示(v0.8.0)

 M5Stack UI Flow の v0.8.0 がリリースされて、簡単に画像が表示できるようになったようなので、試してみました。 公式のツイートはこちら。

 UI Flow の基本的な環境設定についてはこちらもどうぞ。

blog.akanumahiroaki.com

画像のアップロード

 まずは表示したい画像ファイルをアップロードします。 v0.8.0 では画面右上にファイルをアップロードするためのメニューが追加されていますので、これをクリックします。

f:id:akanuma-hiroaki:20181020201534p:plain

 すると下記のダイアログが表示されますので、 Images を選択します。 Blocklys の方は特にファイルのアップロードができるようにはなっていないようなので、今後機能が追加されていくのかもしれません。 Images の方ではすでにアップロード済みのファイルがあればリストが表示されます。アップロードしたファイルは画面をリロードしたり、ブラウザを閉じて再度アクセスした時にも保存されているようです。新しい画像ファイルをアップロードするには、 Add Image ボタンをクリックして、ローカルのファイルを選択してアップロードします。

f:id:akanuma-hiroaki:20181020202043p:plain

 ちなみにアップロードできる画像ファイルは JPEG のみで、25KB以下のものに制限されています。また、ファイル名も10文字以下という制約があります。

f:id:akanuma-hiroaki:20181020202626p:plain

f:id:akanuma-hiroaki:20181020202639p:plain

画像の配置

 画像がアップロードできたら次は画面に画像を配置します。 v0.8.0 では画面のコンポーネントに画像ファイルが追加されていますので、これをドラッグ&ドロップで配置します。

f:id:akanuma-hiroaki:20181020203354p:plain

 配置したモジュールを選択するとプロパティが表示されますので、 imgName のプロパティでアップロード済みの画像ファイルの中から表示したいものを選択します。

f:id:akanuma-hiroaki:20181020203736p:plain

 この状態で実行すると、下記画像のように画像ファイルが表示されます。 UI Flow の画面上で配置した画像コンポーネントには実際の画像サイズは反映されないので、実際の表示は実行して確認する必要があります。

f:id:akanuma-hiroaki:20181020204801j:plain

 他のコンポーネントと組み合わせて表示させれば、簡単に画面を構成することができます。ちなみに v0.8.0 ではラベルのフォントが選択できるようになっています。ラベルのプロパティにフォントのプロパティが追加されていますので、使用したいフォントを選択します。

f:id:akanuma-hiroaki:20181020204518p:plain

 上記の内容で実行した様子は下記画像のようになります。

f:id:akanuma-hiroaki:20181020204824j:plain

 とりあえず画像を表示することはできましたが、確認した限りではロジックの中で扱うことはまだできないようなので、静的な表示に限定されそうです。

m5cloud の場合

 ちなみに m5cloud で同様に画像を表示するとしたら、画像ファイルを m5cloud のメニューからアップロードした上で、下記のようなコードを実行すると大体同じような表示をすることができます。タイトルやラベルの表示はちょっと面倒ですが、画像の配置については lcd.image() で lcd.CENTER や lcd.BOTTOM などの指定をすることができるので、 UI Flow よりも位置が調整しやすいですし、ロジックの中に組み込んで使うことができますので、まだまだこちらの方が実用的ですね。

from m5stack import lcd

lcd.clear()
lcd.setCursor(0, 0)
lcd.setColor(lcd.WHITE, lcd.BLUE)

fw, fh = lcd.fontSize()
ww, wh = lcd.winsize()

lcd.rect(0, 0, ww, fh + 1, lcd.BLUE, lcd.BLUE)
lcd.println("Photo Album")

lcd.font(lcd.FONT_DejaVu24)
lcd.setColor(lcd.WHITE, lcd.BLACK)
lcd.print('My Cat', 10, 30)

lcd.image(lcd.CENTER, lcd.BOTTOM, 'IMG_s.JPG')

まとめ

 UI Flow v0.8.0 で画像を扱うことができるようになり、画像を表示するだけであればコードを書かずに実現できるようになりましたが、まだ静的な表示に限られるので使用用途は限られますね。ただ UI Flow は短いスパンでどんどんアップデートされてきているので、画像についても今後様々な使い方ができるようになってくるかと思います。Remote Config 等と組み合わせられると面白い気もするので、今後の機能追加に期待したいですね。

M5Stack で MicroPython のスレッドを使う

 前回は M5Stack でテキストを簡易的にスクロール表示させる処理を実装してみましたが、画面の下端にテキストをスクロール表示させつつ、残りの部分に何かを表示するにはスレッドを使った処理が必要かと思ったので、今回は M5Stack の _thread モジュールを使った処理を実装してみました。

 _thread モジュールについては下記サイトを参考にさせていただきました。

qiita.com

 また、 M5Stack の github リポジトリにもサンプルが公開されていました。

github.com

サンプル実装

 まずは _thread モジュールがちゃんと使えることを確認するために、ごく簡単なサンプルを実装してみます。下記のコードではテキストを表示する2つのスレッドを生成し、違う間隔でテキストの表示を行います。 _thread.start_new_thread() でメソッドを指定してスレッドを生成しています。

from m5stack import lcd

import time
import _thread

lcd.clear()
lcd.setCursor(0, 0)
lcd.setColor(lcd.WHITE)

def hello():
    while True:
        time.sleep(3)
        lcd.println("Hello World! from: {}".format(_thread.getSelfName()))

def goodby():
    while True:
        time.sleep(5)
        lcd.println("Goodby! from: {}".format(_thread.getSelfName()))
    
_thread.start_new_thread('hello', hello, ())
_thread.start_new_thread('goodby', goodby, ())

 これを実行すると下記の動画のようになります。とりあえず各スレッドでの処理が行われていることが確認できます。

天気情報+アバター表示

 それでは次は前回の天気情報のスクロール表示にアバター表示を組み合わせて、天気情報をしゃべっているような表示を実装してみたいと思います。M5Stack でのアバター表示は @meganetaaan さんが m5stack-avatar を公開されていますので、 Arduino 環境であればこちらを使うのが良いかと思います。

github.com

 検索してみた限りではまだ MicroPython 版のアバター表示ライブラリは内容でしたので、自前で簡易に表示させてみたいと思います。画面イメージは下記画像の通りです。

f:id:akanuma-hiroaki:20181013223503j:plain

 まずはコード全体を掲載しておきます。

from m5stack import lcd

import random
import time
import ujson
import urequests
import _thread

class Weather:
    def __init__(self):
        self.base_url    = 'http://api.openweathermap.org/data/2.5/weather?q={},jp&appid={}'
        self.api_key     = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
        self.prefectures = ['Tokyo', 'Saitama', 'Nagoya']

    def get_weather(self, prefecture):
        response      = urequests.get(self.base_url.format(prefecture, self.api_key))
        json          = response.json()
        main          = json['main']
        self.temp     = main['temp']
        self.pressure = main['pressure']
        self.humidity = main['humidity']
        self.temp_min = main['temp_min']
        self.temp_max = main['temp_max']

    def text(self, prefecture):
        return "[{}] temp: {} pressure: {} humidity: {} temp_min: {} temp_max: {}".format(
            prefecture, self.temp, self.pressure, self.humidity, self.temp_min, self.temp_max
        )

class Face:
    def __init__(self, ww, wh, fw, fh):
        self.ww                 = ww
        self.wh                 = wh
        self.fw                 = fw
        self.fh                 = fh
        self.eye_x              = 90
        self.eye_y              = 80
        self.eye_r              = 10
        self.eye_close_x        = 70
        self.eye_close_width    = 40
        self.eye_close_height   = 5
        self.blink_term_ms      = 500
        self.mouth_x            = 135
        self.mouth_y            = 150
        self.mouth_width        = 50
        self.mouth_height       = 5
        self.mouth_close        = True
        self.mouth_close_height = 20
        
        self.spaces = ' '
        while lcd.textWidth(self.spaces) < self.ww:
            self.spaces += ' '

    def blink(self):
        while True:
            self.eye_close()
            time.sleep_ms(self.blink_term_ms)
            self.eye_open()
            time.sleep(random.randint(2, 6))

    def eye_close(self):
        lcd.circle(self.eye_x, self.eye_y, self.eye_r, lcd.BLACK, lcd.BLACK)
        lcd.circle(self.ww - self.eye_x, self.eye_y, self.eye_r, lcd.BLACK, lcd.BLACK)
        lcd.rect(self.eye_close_x, self.eye_y, self.eye_close_width, self.eye_close_height, lcd.WHITE, lcd.WHITE)
        lcd.rect(
            self.ww - self.eye_close_x - self.eye_close_width,
            self.eye_y, self.eye_close_width,
            self.eye_close_height,
            lcd.WHITE,
            lcd.WHITE
        )

    def eye_open(self):
        lcd.rect(self.eye_close_x, self.eye_y, self.eye_close_width, self.eye_close_height, lcd.BLACK, lcd.BLACK)
        lcd.rect(
            self.ww - self.eye_close_x - self.eye_close_width,
            self.eye_y,
            self.eye_close_width,
            self.eye_close_height,
            lcd.BLACK,
            lcd.BLACK
        )
        lcd.circle(self.eye_x, self.eye_y, self.eye_r, lcd.WHITE, lcd.WHITE)
        lcd.circle(self.ww - self.eye_x, self.eye_y, self.eye_r, lcd.WHITE, lcd.WHITE)
        
    def lipsync(self):
        if self.mouth_close:
            self.lip_open()
        else:
            self.lip_close()

    def lip_close(self):
        lcd.rect(
            self.mouth_x,
            self.mouth_y - (self.mouth_close_height // 2),
            self.mouth_width,
            self.mouth_height + self.mouth_close_height, 
            lcd.BLACK,
            lcd.BLACK
        )
        lcd.rect(self.mouth_x, self.mouth_y, self.mouth_width, self.mouth_height, lcd.WHITE, lcd.WHITE)
        self.mouth_close = True

    def lip_open(self):
        lcd.rect(self.mouth_x, self.mouth_y, self.mouth_width, self.mouth_height, lcd.BLACK, lcd.BLACK)
        lcd.rect(
            self.mouth_x,
            self.mouth_y - (self.mouth_close_height // 2),
            self.mouth_width,
            self.mouth_height + self.mouth_close_height,
            lcd.WHITE,
            lcd.WHITE
        )
        self.mouth_close = False

    def speak(self, text):
        lcd.textClear(0, (self.wh - self.fh) - 1, self.spaces)
        lcd.print(text, 0, (self.wh - self.fh) - 1)
        time.sleep_ms(3000)
        while lcd.textWidth(text) > 0:
            text = text[1:]
            lcd.textClear(0, (self.wh - self.fh) - 1, self.spaces)
            lcd.print(text, 0, (self.wh - self.fh) - 1)
            self.lipsync()
            time.sleep_ms(200)
        self.lip_close()

    def mouth(self):
        lcd.rect(self.mouth_x, self.mouth_y, self.mouth_width, self.mouth_height, lcd.WHITE, lcd.WHITE)
        while True:
            typ, sender, msg = _thread.getmsg()
            if msg:
                self.speak(msg)
            time.sleep_ms(200)

    def display(self):
        _thread.start_new_thread('eye', self.blink, ())
        self.mouth_thread_id = _thread.start_new_thread('mouth', self.mouth, ())
        while True:
            typ, sender, msg = _thread.getmsg()
            if msg:
                _thread.sendmsg(self.mouth_thread_id, msg)
            time.sleep_ms(200)

def display_weather(face_thread_id):
    weather = Weather()
    while True:
        for prefecture in weather.prefectures:
            weather.get_weather(prefecture)
            text = weather.text(prefecture)
            _thread.sendmsg(face_thread_id, text)
            time.sleep(30)

lcd.setCursor(0, 0)
lcd.setColor(lcd.WHITE)
lcd.font(lcd.FONT_DejaVu24)
lcd.clear()

fw, fh = lcd.fontSize()
ww, wh = lcd.winsize()

face = Face(ww, wh, fw, fh)

face_thread_id = _thread.start_new_thread('face', face.display, ())
_thread.start_new_thread('weather', display_weather, (face_thread_id,))

 Weather クラスの内容は前回と同様です。

 Face クラスでは顔の表示と瞬き表示と、口パクとともにテキストをスクロール表示する処理を行います。

 処理の流れとしては、 Face クラスのインスタンスを生成し、その display() メソッドを実行するスレッドを開始します。 display() メソッドではさらに口の動きと独立して瞬き表示を行うためのスレッドと、口の処理を行うスレッドを開始します。

 続いて天気情報を表示するためのスレッドを開始し、メソッドの引数には上記の display() メソッドを実行しているスレッドのIDを渡します。天気情報を取得してテキストを生成したら、そのスレッドIDを指定してテキストを _thread.sendmsg() メソッドでメッセージとして送信します。

 display() メソッドではメッセージを受け取ったらそのメッセージをさらに口の動きを処理しているスレッドに通知し、受け取った側の mouth() メソッドでテキストの表示と口パクの処理を行なっています。口パクの表示ではひとまず今回は1文字スクロールする度に口の開閉を切り替えるようにしてみました。

 目や口の開閉の切り替えは、調べた感じでは描画したオブジェクトを消すという処理はなさそうだったので、背景色と同じ色で描画し直すことで見えなくしてから切り替え後の状態を描画するようにしています。

 図形の表示については下記に API の説明が記載されています。

github.com

動作確認

 上記のコードを実行すると下記の動画のようになります。

 テキストの内容が天気情報なので微妙なところはありますが、それでも口パクがつくだけでアバターが喋ってるような感じに見えますね。 

まとめ

 スレッド処理はそんなに詳しいわけではないので、細かいことはあまり考慮していなかったり、まだまだ実装も適当ですが、とりあえず動かすことができました。ただ、うまく使っていかないと実装がどんどん複雑になっていってしまいそうですし、デバッグ等も難しくなりそうなので、極力シンプルに最低限で使うようにすべきかなと思います。アバター表示はそれっぽいのもができたので、もっと実用的な機能や面白い機能も追加していってアシスタントっぽくできると面白そうかなと考えています。

M5Stack でテキストをスクロール表示(MicroPython)

 M5Stack で今後色々作っていきたいと思ってるわけですが、デジタルサイネージっぽくスクロール表示させたいケースがあり、どうやったらスクロール表示できるかやってみました。簡易的な実装なのでカクカクしますが、一応スクロールするようになったので内容載せておきます。

表示位置の指定

 M5Stack で長い文章を表示する場合、普通に lcd.print() 等を使用すると、画面の端まで行くと折り返して次の行に表示されます。

f:id:akanuma-hiroaki:20181006223121j:plain

 表示開始位置はデフォルトでは左上になりますが、 lcd.print() 等のテキスト表示用メソッドの引数に座標情報を渡すと表示開始位置を指定することができます。

 また、デフォルトでは表示領域(Window)が全画面に設定されていますが、画面の一部を表示領域とすることもできますので、下記のようなメソッドで表示領域を指定し、その中での相対座標で表示位置を指定することもできます。

 今回は画面の下端1行分を表示領域として、はみ出るテキストをスクロールして表示する形にしてみます。表示領域を指定するには lcd.setwin(x, y, x1, y1) メソッドを使って、左上の座標(x, y)と右下の座標(x1, y1)の矩形で指定します。

 まずフォントサイズとスクリーンサイズを取得し、画面左下からフォントの高さ分を引いたところを左上の座標とし、画面右下を表示領域の右下の座標とします。また、座標は0スタートなので、サイズの値からそれぞれ -1 しています。

from m5stack import lcd

lcd.clear()
lcd.setCursor(0, 0)
lcd.setColor(lcd.WHITE)
lcd.font(lcd.FONT_DejaVu24)

text = "Hello World! This is a sample code of text scrolling."

fw, fh = lcd.fontSize()
sw, sh = lcd.screensize()

lcd.setwin(0, (sh - fh) - 1, sw - 1, sh - 1)

lcd.print(text, 0, 0)

 これを実行すると下記のように画面下端に1行分テキストが表示されます。

f:id:akanuma-hiroaki:20181006223150j:plain

スクロール表示

 今回はカクついてても最低限のスクロール表示ができればOKという考え方で、シンプルな実装にしています。方針としては、テキストの長さが表示領域の幅よりも大きい時は、テキストの先頭1文字を削除して再度表示するだけの単純なものです。先ほどのコードに time モジュールの import と、 time.sleep_ms(3000) 以降のループ処理を追加しています。

from m5stack import lcd

import time

lcd.clear()
lcd.setCursor(0, 0)
lcd.setColor(lcd.WHITE)
lcd.font(lcd.FONT_DejaVu24)

text = "Hello World! This is a sample code of text scrolling."

fw, fh = lcd.fontSize()
sw, sh = lcd.screensize()

lcd.setwin(0, (sh - fh) - 1, sw - 1, sh - 1)
lcd.print(text, 0, 0)

time.sleep_ms(3000)
while lcd.textWidth(text) > 0:
    text = text[1:]
    lcd.clearwin()
    lcd.print(text, 0, 0)
    time.sleep_ms(200)

 これを実行すると下記のような表示になります。

天気情報の表示サンプル

 ではもう少しデジタルサイネージっぽく、以前の記事で使った天気情報を、1行でスクロール表示するようにしてみたいと思います。

blog.akanumahiroaki.com

 天気情報APIからのデータ取得については上記記事をご参照いただくとして、今回は下記のような実装にしています。

from m5stack import lcd

import time
import ujson
import urequests

class Weather:
    def __init__(self):
        self.base_url    = 'http://api.openweathermap.org/data/2.5/weather?q={},jp&appid={}'
        self.api_key     = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
        self.prefectures = ['Tokyo', 'Saitama', 'Nagoya']

    def get_weather(self, prefecture):
        response = urequests.get(self.base_url.format(prefecture, self.api_key))
        json = response.json()
        main = json['main']
        self.temp     = main['temp']
        self.pressure = main['pressure']
        self.humidity = main['humidity']
        self.temp_min = main['temp_min']
        self.temp_max = main['temp_max']

    def text(self, prefecture):
        return "[{}] temp: {} pressure: {} humidity: {} temp_min: {} temp_max: {}".format(
            prefecture, self.temp, self.pressure, self.humidity, self.temp_min, self.temp_max
        )

def print_scroll(text):
    lcd.clearwin()
    lcd.print(text, 0, 0)
    time.sleep_ms(3000)
    while lcd.textWidth(text) > 0:
        text = text[1:]
        lcd.clearwin()
        lcd.print(text, 0, 0)
        time.sleep_ms(200)

lcd.setCursor(0, 0)
lcd.setColor(lcd.WHITE)
lcd.font(lcd.FONT_DejaVu24)
lcd.clear()

fw, fh = lcd.fontSize()
ww, wh = lcd.winsize()

weather = Weather()
while True:
    for prefecture in weather.prefectures:
        weather.get_weather(prefecture)
        text = weather.text(prefecture)
        lcd.setwin(0, (wh - fh) - 1, ww - 1, wh - 1)
        print_scroll(text)
        time.sleep_ms(2000)

 Weather クラスの内容はデータ取得処理等はほとんど変わっていませんが、画面表示用の処理は外に切り出し、データを1行にまとめたテキストを返すようにしています。そしてそのテキストを先ほどのスクロール処理で表示しています。これを実行すると下記のように天気情報がスクロールで表示されます。

まとめ

 とりあえずのスクロール処理は思ったより簡単に実装できましたが、もっと滑らかにスクロールしようと思うと単純なテキスト処理だけだとできなそうな気がするので、結構ハードルが高そうではあります。また、今回はテキスト表示の処理しかしてませんが、他の内容を画面上部に表示しつつ下部にスクロール表示となると、スレッド処理的なものが必要そうになるので難しそうですが、今後トライしてみようと思います。

M5Stack UI Flow Remote Function を試す

 M5Stack UI Flow に Remote Function というのがあって面白そうだったので試してみました。Remote Function はスマートフォンなどのブラウザから M5Stack を操作するための UI を提供するもので、下記ツイートで紹介されていました。

UI Flow の環境

 UI Flow の環境設定については以前この記事にも書きましたので参照いただければと思います。

blog.akanumahiroaki.com

 今回は UI Flow のバージョンについては 2018/09/29 時点で最新だった v0.7 を使用しています。

使用方法

 Remote Function は UI Flow のメニューの一番下に配置されています。

f:id:akanuma-hiroaki:20180929202857p:plain

 Remote Function のブロックは下記のようなものが用意されています。

 一つ目のブロックは、 M5Stack の LCD に Remote UI の URL の QR コードを表示するためのものです。

 二つ目のブロックは Remote UI にボタンを表示し、押された時の動作を設定するものです。

 三つ目のブロックは Remote UI にラベルを表示するためのものです。

f:id:akanuma-hiroaki:20180929203442p:plain

 今回は Remote UI でボタンが押されたら変数のステータスを切り替えて M5Stack の LCD に表示するものを作ってみます。実際は LED 等を組み合わせて点灯の状態を切り替えたりするイメージです。

 M5Stack 側の表示は下記のようにしてみました。画面下部の STAUTS: の右側の TEXT 部分にステータスが True/False で表示されます。画面中央部には Remote UI アクセス用の QR コードが表示されます。

f:id:akanuma-hiroaki:20180929204547p:plain

 ブロックは下記のように構成しました。 QR コードを表示した後、 Remote UI に Change Status ボタンを配置します。ボタンが押されたら status 変数の中身を切り替えて、 M5Stack の LCD の表示に反映します。最後に Remote UI にも status 変数の中身を表示するラベルを配置します。

f:id:akanuma-hiroaki:20180929204748p:plain

動作確認

 上記のブロックを M5Stack で実行すると、下記のような画面が表示されます。

f:id:akanuma-hiroaki:20180929212426j:plain

 この QR コードをスマートフォン等で読み取って Remote UI にアクセスします。私が試した限りでは上記 QR コードはうまく読み取れなかったのですが、 Remote UI にアクセスするための QR コードは UI Flow の右上のボタンをクリックすることでも表示できます。(下記画像は読み取れないように加工してあります。)

f:id:akanuma-hiroaki:20180929211736p:plain

 QR コードを読み取って Remote UI にアクセスすると下記のような画面が表示されます。ボタンやラベルの色はランダムに決まっているようです。

f:id:akanuma-hiroaki:20180929212605p:plain:w300

 Change Status ボタンをタップすると、 M5Stack の LCD の STATUS 表示が切り替わります。 Remote UI 側はリロードするとその時の status 変数の内容が反映されますが、試した限りではリアルタイムでは反映されませんでした。

まとめ

 全て自前でリモートのUIの環境を用意しようと思うと、サーバを用意したり、スマートフォンアプリもしくはWebのUIを実装するなどかなり手間がかかると思いますが、こんなに簡単にリモートのUIが使えるのはすごいですね。もちろん凝ったUIは作れませんが、シンプルなUIでも十分なケースは多いですし、プロトタイピングやイベント等での一時利用にはすごく便利だと思います。 UI Flow 自体は v0.7 でもまだファイルが保存できなかったり、まだまだ足りてない部分も多いと思いますが、今後のアップデートに期待したいです。

M5Stack で天気情報を表示する(OpenWeatherMap API)

 将来的には M5Stack で Google Calendar に登録してあるスケジュール情報を表示させたいと思っているのですが、認証周りなどが少しハードル高そうなので、まずは認証なしで情報を取ってこられる API から情報を取得して表示する処理を試してみたいと思います。その中でもそれなりに実用性がありそうなものとして、天気情報を取得して画面に表示する処理を作ってみました。

天気情報API

 ひとまず今回は無料で手軽に試せる API を探したところ、 OpenWeatherMap という API がみつかったのでこれを利用してみます。

openweathermap.org

 使い方についてはこちらも参考にさせていただきました。

qiita.com

 ユーザ登録をすると API Key が発行されるのでそれを使用します。アクセスする URL は下記のようになります。

http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp&appid=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

 まずはブラウザでアクセスして確認してみて、情報が取得できていれば OK です。

ファームウェア実装

 MicroPython は基本的には Python3 と互換ですが、 Python の全てのモジュールが移植されているわけではありませんので、 API ドキュメント等を参照して Python との違いを確認しながら実装する必要があります。今回は天気情報の API にリクエストを送信して結果を取得するために、 MicroPython の urequests モジュールを使用しています。また、結果を json オブジェクトとして扱うためには ujson モジュールも必要になりますので、こちらも import しておきます。

 urequests の使い方についてはこちらのサイトを参考にさせていただきました。

blog.boochow.com

 ソース全体は下記のようになります。 self.api_key には実際に取得した API Key を設定します。対象の都道府県としては今回はとりあえず東京、埼玉、名古屋を指定しています。

from m5stack import lcd

import time
import ujson
import urequests

class Weather:
    def __init__(self):
        self.base_url = 'http://api.openweathermap.org/data/2.5/weather?q={},jp&appid={}'
        self.api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
        self.prefectures = ['Tokyo', 'Saitama', 'Nagoya']

        lcd.setCursor(0, 0)
        lcd.setColor(lcd.WHITE)
        lcd.font(lcd.FONT_DejaVu24)
        self.fw, self.fh = lcd.fontSize()

    def get_weather(self, prefecture):
        response = urequests.get(self.base_url.format(prefecture, self.api_key))
        json = response.json()
        main = json['main']
        self.temp = main['temp']
        self.pressure = main['pressure']
        self.humidity = main['humidity']
        self.temp_min = main['temp_min']
        self.temp_max = main['temp_max']

    def display(self, prefecture):
        lcd.clear()
        lcd.print(prefecture, 0, self.fw * 0)
        lcd.print("temp: {}".format(self.temp), 10, self.fw * 1)
        lcd.print("pressure: {}".format(self.pressure), 10, self.fw * 2)
        lcd.print("humidity: {}".format(self.humidity), 10, self.fw * 3)
        lcd.print("temp_min: {}".format(self.temp_min), 10, self.fw * 4)
        lcd.print("temp_max: {}".format(self.temp_max), 10, self.fw * 5)

weather = Weather()
while True:
    for prefecture in weather.prefectures:
        weather.get_weather(prefecture)
        weather.display(prefecture)
        time.sleep(10)

動作確認

 それでは動作を確認してみます。上記コードを m5cloud から M5Stack にアップロードすると、10秒おきに東京、埼玉、名古屋の天気情報を API から取得して、順番に表示していきます。ちなみに API から取得できる温度は華氏表記になりますので、実際のサービスに使用するときには適宜変換する必要があるかと思います。

f:id:akanuma-hiroaki:20180922235422j:plain

f:id:akanuma-hiroaki:20180922235438j:plain

f:id:akanuma-hiroaki:20180922235455j:plain

まとめ

 認証不要で単純に情報を取得するだけであれば(API Key は必要ですが) API にリクエストを投げて処理を行うのも urequests を使用して簡単に行うことができました。 Google Calendar 等を使うには OAuth 等の認証を行う必要があるのですが、M5Stack でそれを行う方法はまだわかっていないので、調べてみたいと思います。