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 でそれを行う方法はまだわかっていないので、調べてみたいと思います。

M5Stack クレードルの温湿度センサー(DHT12)を使ってみる

 以前この記事でも書きましたが、 M5Stack を Maker Faire Tokyo 2018 のスイッチサイエンスさんのブースで購入した時に、特典としてクレードルをいただきました。

blog.akanumahiroaki.com

 このクレードルには温湿度センサー DHT12 が付属していたようなので、今回は MicroPython で DHT12 の値を読み取って表示してみたいと思います。ちなみにこのクレードルは単品でもこちらで販売されているようです。

Buy Products Online from China Wholesalers at Aliexpress.com

ハードウェア

 クレードルの裏面は下記写真のようになっています。右下に埋め込まれている青いものが DHT12 です。

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

 クレードルは M5Stack の下部のピンに接続されます。今回は DHT12 との接続にクレードル経由で I2C を使用しますので、 SDA, SCL を使用します。ピン番号は上部のピンと同じになりますので、SDA が 21、 SCL が 22 になります。

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

 クレードルと DHT12 は、ちょっと基板での記載部分が見辛いですが、左から SCL, GND, SDA, 3V3 が接続されています。

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

ファームウェア

 今回は m5cloud を使用して MicroPython でファームウェアを書いていきます。 DHT12 の使用については、 m5cloud のリポジトリにライブラリが公開されていました。

github.com

 サンプルについても同じリポジトリに公開されていました。 SDA, SCL のピン番号も 21, 22 になっているので、これをこのまま使用できます。

github.com

 これを dht12.py として m5cloud 上にアップロードもしくはコピペで配置します。

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

 そして main.py は下記のような内容にしました。温湿度計ということで Hygrothermograph に処理内容をまとめ、コンストラクタで RTC を同期し、 DHT12 を初期化しています。フォントの指定もしておきます。

 measure() メソッドでは DHT12 から温度と湿度を取得し、インスタンス変数に格納しておきます。

 display() メソッドでは LCD に時刻、温度、湿度を表示しています。

from m5stack import lcd
from machine import RTC
import time
import dht12

class Hygrothermograph:
    def __init__(self):
        rtc = RTC()
        rtc.ntp_sync('ntp.nict.jp', tz = 'JST-9')
        for i in range(100):
            if rtc.synced():
                break
            time.sleep_ms(10)
            
        self.sensor = dht12.DHT12()
        
        lcd.font(lcd.FONT_DejaVu24)
        lcd.setColor(lcd.WHITE)
        self.fw, self.fh = lcd.fontSize()
        
    def measure(self):
        self.sensor.measure()
        self.temperature = self.sensor.temperature()
        self.humidity = self.sensor.humidity()

    def display(self):
        lcd.clear()
        lcd.print("Time:", 0, self.fh * 0)
        lcd.print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), 10, self.fh * 1)
        lcd.print("Temperature:", 0, self.fh * 2)
        lcd.print("{} deg.".format(self.temperature), 10, self.fh * 3)
        lcd.print("Humidity:", 0, self.fh * 4)
        lcd.print("{} %".format(self.humidity), 10, self.fh * 5)

hygrothermograph = Hygrothermograph()

while True:
    hygrothermograph.measure()
    hygrothermograph.display()
    time.sleep(10)

実行結果

 それでは実行してみます。 m5cloud から upload ボタンをクリックして本体にアップロードして実行します。結果は下記の写真のようになり、時刻、温度、湿度が表示されて10秒毎に更新されていきます。

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

 ちなみにクレードルの DHT12 は本体のすぐ下にあって、本体の熱の影響を受けると思いますので、実際の温度を測るには何らか工夫する必要があると思われます。

まとめ

 M5Stack の LCD にすぐに確認したい内容を表示させている場合、クレードルは見やすい角度で本体の位置をキープできるのでとても良い感じですね。とりあえず m5cloud と MicroPython でセンサーの値を読み出すこともできたので、画面表示の方も今後色々と試してみたいと思います。

M5Stack UI Flow を Mac で使う

 以前に下記ツイートを見てから試してみたいと思ってた UI Flow を試してみました。

 UI Flow は Scratch のようにブロックを並べることでプログラミングができ、画面レイアウトもパーツを並べることで行うことができるようになっています。ブロックではなく Micro Python でコーディングをすることもできます。

 チュートリアルとしては下記のページを参考にさせてもらいました。

forum.m5stack.com

ファームウェアのセットアップ

 UI Flow を使うにはそれに対応したファームウェアを使う必要があります。ファームウェアを簡単に選択してフラッシュすることができる M5Burner というツールが用意されていて、 UI Flow 用のファームウェアを選択することもできるのですが、このツールは Windows 用にしか用意されていないので、 Mac ではそのまま使うことができません。ですが、 M5Burner をダウンロードして解凍するとその中にファームウェアのイメージは含まれていて、フラッシュ用のコマンドが書かれた sh ファイルも含まれているので、これを利用することで UI Flow を使うことができます。

 M5Burner は M5Stack のサイトのダウンロードページからダウンロードすることができます。

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

 ですが 2018/09/08 に試した時点では、ここからダウンロードできる zip に含まれている UI Flow 用のファームウェアはバージョン 0.4 までで、このバージョンだと UI Flow からプログラムを実行するところでうまく動きませんでした。

 そこで UI Flow の設定画面から取得した zip にはバージョン 0.5 まで含まれており、そちらであれば実行まで動作させることができたのでそちらを使用します。

 まずは M5UI.Flow にアクセスします。すると API Key 設定用のダイアログが表示されますが、まだ API Key はわからないので Skip します。

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

 次に画面右上の設定ボタンをクリックして設定ダイアログを開きます。

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

 API Key の設定フォームとファームウェア(M5Burner)のダウンロードリンクが表示されますので、ダウンロードリンクをコピーします。

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

 ここからはターミナルで実行します。まずは wget で先ほどコピーしたダウンロードリンクから M5Burner をダウンロードします。

$ wget http://flow.m5stack.com/download/M5Burner-flow-only.zip
--2018-09-08 11:36:33--  http://flow.m5stack.com/download/M5Burner-flow-only.zip
Resolving flow.m5stack.com... 52.230.67.99
Connecting to flow.m5stack.com|52.230.67.99|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 12199380 (12M) [application/zip]
Saving to: 'M5Burner-flow-only.zip'

M5Burner-flow-only.zip                                      100%[===========================================================================================================================================>]  11.63M  2.65MB/s   in 4.6s   

2018-09-08 11:36:38 (2.53 MB/s) - 'M5Burner-flow-only.zip' saved [12199380/12199380]

 そして解凍します。

$ unzip M5Burner-flow-only.zip

 すると下記のようファイルが展開されます。

$ ls -l
total 27152
-rw-r--r--  1 akanuma  staff  12199380 Sep  6 20:38 M5Burner-flow-only.zip
-rw-r--r--  1 akanuma  staff     31232 Aug 21 16:42 M5Burner.exe
-rw-r--r--  1 akanuma  staff       189 Jun  9 13:37 M5Burner.exe.config
-rw-r--r--  1 akanuma  staff     34304 Aug 21 16:42 M5Burner.pdb
-rw-r--r--  1 akanuma  staff    662528 Mar 24 17:44 Newtonsoft.Json.dll
-rw-r--r--  1 akanuma  staff    684778 Mar 24 17:44 Newtonsoft.Json.xml
drwxr-xr-x  4 akanuma  staff       128 Sep  6 19:14 firmwares
drwxr-xr-x  3 akanuma  staff        96 Sep  3 15:00 tools
-rw-r--r--  1 akanuma  staff       169 Aug 31 19:51 update.log

 使用するのは firmwares ディレクトリ配下のファイルだけで、 UI Flow 用のファームウェアの最新バージョンのディレクトリへ移動します。

$ cd firmwares/M5Flow/v0.5/
$ ls -l
total 8152
-rw-r--r--  1 akanuma  staff  1351152 Aug 31 19:16 firmware_0x1000.bin
-rw-r--r--  1 akanuma  staff      335 Sep  6 19:11 flash.sh
-rw-r--r--  1 akanuma  staff  2818048 Sep  6 19:05 spiffs_image_0x150000.img

 ここに flash.sh というファイルがあり、この中身はファームウェアをフラッシュするためのコマンドが書かれたものになっています。

$ cat flash.sh 
#!/bin/bash
esptool.py --chip esp32 --port /dev/tty.SLAB_USBtoUART --baud 961200 write_flash -z 0x1000 firmware_0x1000.bin
esptool.py --chip esp32 --port /dev/tty.SLAB_USBtoUART --baud 921600 --before default_reset --after no_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x150000 spiffs_image_0x150000.img

 このファイルに実行権限をつけて実行してしまっても良いのですが、それぞれのコマンドの実行結果を確認したかったので一つずつ実行します。 M5Stack を USB ケーブルで接続していれば下記のようにデバイスファイルが存在します。

$ ls -l /dev/tty.SLAB_USBtoUART 
crw-rw-rw-  1 root  wheel   21,  24 Sep  8 11:03 /dev/tty.SLAB_USBtoUART

 デバイスファイルが確認できたら、まず一つ目のコマンドで bin ファイルを M5Stack に書き込みます。

$ esptool.py --chip esp32 --port /dev/tty.SLAB_USBtoUART --baud 961200 write_flash -z 0x1000 firmware_0x1000.bin
esptool.py v2.5.0
Serial port /dev/tty.SLAB_USBtoUART
Connecting........___
Chip is ESP32D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse
MAC: 80:7d:3a:c4:71:bc
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 961200
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 1351152 bytes to 841602...
Wrote 1351152 bytes (841602 compressed) at 0x00001000 in 13.8 seconds (effective 784.0 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

 特にエラーはなく実行できたようです。次にもう一つのコマンドを実行し、 img ファイルを書き込みます。

$ esptool.py --chip esp32 --port /dev/tty.SLAB_USBtoUART --baud 921600 --before default_reset --after no_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x150000 spiffs_image_0x150000.img
esptool.py v2.5.0
Serial port /dev/tty.SLAB_USBtoUART
Connecting........_
Chip is ESP32D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse
MAC: 80:7d:3a:c4:71:bc
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 921600
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 2818048 bytes to 667546...
Wrote 2818048 bytes (667546 compressed) at 0x00150000 in 18.7 seconds (effective 1205.6 kbit/s)...
Hash of data verified.

Leaving...
Staying in bootloader.

 こちらも問題なく実行できたようです。これでひとまずファームウェアの書き込みは行えたので、これ以降は Windows 環境で実行している場合と同様かと思います。

Wi-Fi 接続設定

 ファームウェアの書き込みが終了したら USB ケーブルを抜くか、 M5Stack 本体横のボタンで M5Stack を再起動すると UI Flow のロゴが表示されます。

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

 そしてそのまま何もしなければ、標準でじゃんけんゲームのようなものが書き込まれているで、それが実行されます。

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

 Wi-Fi の設定をするためにはもう一度 M5Stack を再起動して、 UI Flow のロゴが表示されている間に右のボタンを長押しします。

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

 設定メニューが表示されますので、 set reconnect wifi を選択して真ん中のボタンを押します。

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

 すると M5Stack が Wi-Fi AP として動作し、画面上に SSID とアクセス先 IP アドレスが表示されますので、 Mac からその AP に接続し、ブラウザから対象の IP アドレスにアクセスします。

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

 下記のように M5Stack が接続する AP を選択する画面が表示されますので、 AP を選択してパスワードを入力し、 Configure をクリックします。

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

 接続が成功すると下記のような画面になり、 M5Stack が再起動されます。

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

UI Flow から M5Stack への接続

 M5Stack を再起動して UI Flow のロゴが表示されている間に、左のボタンを長押しします。

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

 M5Stack から設定済み AP への接続が成功すると、下記のように API Key が表示されます。

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

 これを UI Flow の設定画面から設定します。

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

 API Key を設定しただけでは接続状態が画面に反映されていないので、画面左の M5Stack のイメージの Offline となっているところをクリックします。

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

 接続に成功すると表示が Online に変わります。

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

UI Flow でのプログラミング

 ここまででファームウェアの設定や接続等の環境の準備は完了なので、 UI Flow から簡単なプログラムを実装して実行してみます。まずは M5Stack のイメージの上部にある Label をイメージのディスプレイ上にドラッグ&ドロップします。

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

 ブロックリストの Component -> Label から、 Label にテキストを表示するためのブロックを選択します。

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

 元々配置されている Setup ブロックに選択したブロックを接続し、表示するテキストを変更します。画面にテキストを表示させるだけであればこれだけでOKなので、画面右上の実行ボタンをクリックして、 M5Stack 上で実行します。

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

 下記のように実機の画面にテキストが表示されれば成功です。

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

 テキストを変更して再度実行ボタンをクリックすれば、すぐに実機に反映されます。

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

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

 実行ボタンではその場で実行するだけかと思いますので、開発中は実装と動作確認を繰り返すのには良いかと思いますが、起動時に実行されるようにするには画面右上の書き込みボタンで実機に書き込んでおきます。

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

まとめ

 M5Stack の Micro Python での Web IDE 環境としては M5Cloud もありますが、 UI Flow の利点としては画面イメージを視覚的に確認しながら作成できる点と、ブロックの配置で処理が実装できるので、学習等の目的としてはとても良さそうに思います。ですが現状ではまだ実装したプログラムを保存しておくことなどはできないようですので、今後 IDE としても充実してくることを期待したいと思います。