micropython
M5stack
M5GO

M5GO(M5Stack)で気温・湿度・気圧メーター&時計表示

概要

せっかくの M5GO で色々センサーもついているのに、結局、環境センサーだけで遊んでいます。
今回は、MicroPython での画面表示のお勉強を兼ねて以下の表示を行うプログラムを作成しました。

【2018/12/07更新】動作環境を M5GO Cloud (io.m5go.com Firmware 0.15) から M5 UI.Flow (Firmware v1.0.1-en) への変更に合わせて修正

  • M5GO
  • M5 UI.Flow (Firmware v1.0.1-en)
  • 環境センサー(DHT12、BMP280)
  • MicroPythonで記述
  • アナログメーター風の気温・湿度・気圧計
  • アナログ時計

実行結果

IMG_1475.png

プログラム

env_meter.py
from m5stack import lcd
from dht12   import DHT12
from bmp280  import BMP280
import i2c_bus
import machine
import time
import math
import gc

class Meter:
    def __init__(self, x, y, w, h, tick_s, tick_e, color, value_format):
        # M5 UI.Flow からの実行する場合はここでの import が必要
        # APP.LIST に登録して実行する場合は、この import は不要
        import math

        self.x = x # メーターの表示位置
        self.y = y # メーターの表示位置
        self.w = w # メーターの表示幅
        self.h = h # メーターの表示高
        self.tick_s = tick_s              # 目盛の最小値
        self.tick_e = tick_e              # 目盛の最大値
        self.value_format = value_format  # 値をテキスト表示する際のフォーマット

        self.center_x = x + w // 2        # 針の原点
        self.center_y = y + int(h * 0.9)  # 針の原点
        self.prev_value = tick_s

        lcd.roundrect(x, y, w, h, h // 10, lcd.BLACK, lcd.WHITE)
        lcd.arc(self.center_x, self.center_y, int(h * 0.67), int(h * 0.07), -50, 50, color, color)
        lcd.arc(self.center_x, self.center_y, int(h * 0.6), 2, -50, 50, lcd.BLACK)

        # 目盛の値表示用フォント設定
        lcd.font(lcd.FONT_DefaultSmall, transparent=True)
        fw, fh = lcd.fontSize()

        tick = tick_s
        tick_i = (tick_e - tick_s) // 4
        for r in range(-50, 51, 5):
            if r % 25 == 0:
                # 目盛の最小値から最大値を4分割して目盛値を表示
                lcd.lineByAngle(self.center_x - 1, self.center_y, int(h * 0.6), int(h * 0.1), r, lcd.BLACK)
                lcd.lineByAngle(self.center_x,     self.center_y, int(h * 0.6), int(h * 0.1), r, lcd.BLACK)
                tick_text = str(tick)
                text_width = lcd.textWidth(tick_text)
                lcd.print(tick_text, self.center_x + int(math.sin(math.radians(r)) * h * 0.7) - text_width // 2,
                                     self.center_y - int(math.cos(math.radians(r)) * h * 0.7) - fh,
                                     lcd.BLACK)
                tick += tick_i
            else:
                # 細かい目盛線を表示
                lcd.lineByAngle(self.center_x, self.center_y, int(h * 0.6), int(h * 0.05), r, lcd.BLACK)

    def plot_needle(self, value):
        # 前回取得値の針を消去
        for i in range(-1, 2):
            lcd.lineByAngle(self.center_x + i, self.center_y, int(self.h * 0.15), int(self.h * 0.42),
                            int((self.prev_value - self.tick_s) / (self.tick_e - self.tick_s) * 100 - 50), lcd.WHITE)

        # 今回取得値の針を表示
        for i in range(-1, 2):
            lcd.lineByAngle(self.center_x + i, self.center_y, int(self.h * 0.15), int(self.h * 0.42),
                            int((value           - self.tick_s) / (self.tick_e - self.tick_s) * 100 - 50), lcd.RED)

        # 取得値をテキストでも表示
        lcd.font(lcd.FONT_DejaVu18, transparent=False)
        fw, fh = lcd.fontSize()
        text = self.value_format.format(value)
        lcd.print(text, self.center_x - lcd.textWidth(text) // 2, self.y + self.h - int(fh * 1.2), lcd.BLACK)

        # 針の消去用に値を保存
        self.prev_value = value


class Clock:
    def __init__(self, x, y, w, h, color):
        # M5 UI.Flow からの実行する場合はここでの import が必要
        # APP.LIST に登録して実行する場合は、この import は不要
        import math

        self.x = x # 時計の表示位置
        self.y = y # 時計の表示位置
        self.w = w # 時計の表示幅
        self.h = h # 時計の表示高
        self.center_x = x + w // 2 # 針の中心
        self.center_y = y + h // 2 # 針の中心
        self.hour_deg = 0
        self.minute_deg = 0
        self.second_deg = 0

        lcd.roundrect(x, y, w, h, h // 10, lcd.BLACK, lcd.WHITE)
        # 0 から 360 とは書けないので、半分の円弧を合わせる
        lcd.arc(self.center_x, self.center_y, int(h * 0.39), int(h * 0.08),   0, 180, color, color)
        lcd.arc(self.center_x, self.center_y, int(h * 0.39), int(h * 0.08), 180, 360, color, color)

        lcd.font(lcd.FONT_DefaultSmall, transparent=True)
        fw, fh = lcd.fontSize()
        hour = 12
        for r in range(0, 360, 360 // 60):
            if r % (360 // 12) == 0:
                # 1〜12の位置に黒点および数字を表示
                lcd.circle(self.center_x + int(math.sin(math.radians(r)) * h / 2 * 0.7),
                           self.center_y - int(math.cos(math.radians(r)) * h / 2 * 0.7), 2, lcd.BLACK, lcd.BLACK)
                hour_text = str(hour)
                text_width = lcd.textWidth(hour_text)
                lcd.print(hour_text, self.center_x + int(math.sin(math.radians(r)) * h / 2 * 0.85) - text_width // 2,
                                     self.center_y - int(math.cos(math.radians(r)) * h / 2 * 0.85) - fh // 2,
                                     lcd.BLACK)
                hour = (hour + 1) % 12
            else:
                lcd.pixel(self.center_x + int(math.sin(math.radians(r)) * h / 2 * 0.7),
                          self.center_y - int(math.cos(math.radians(r)) * h / 2 * 0.7), lcd.BLACK)

    def plot_needle(self):
        def needle(n, m, deg, l, color):
            for i in range(n, n + m):
                if deg >= 315 or deg < 45 or deg >= 135 and deg < 225:
                    x, y = i, 0
                else:
                    x, y = 0, i
                lcd.lineByAngle(self.center_x + x, self.center_y + y,
                                0, l, deg, color)

        # 時分秒の各針の角度を計算
        (year, month, mday, hour, minute, second, weekday, yearday) = time.localtime()
        second_deg = second * 6
        minute_deg = minute * 6 + second_deg // 60
        hour_deg   = hour % 12 * 30 + minute_deg // 12

        # 時針の消去(角度が変わっていないときは消さない)
        if hour_deg != self.hour_deg:
            needle(-2, 4, self.hour_deg, int(self.h / 2 * 0.3), lcd.WHITE)
        # 分針の消去(角度が変わっていないときは消さない)
        if minute_deg != self.minute_deg:
            needle(-1, 2, self.minute_deg, int(self.h / 2 * 0.45), lcd.WHITE)
        # 秒針の消去
        needle(0, 1, self.second_deg, int(self.h / 2 * 0.6), lcd.WHITE)

        self.second_deg = second_deg
        self.minute_deg = minute_deg
        self.hour_deg = hour_deg

        # 時針の描画(4本線)
        needle(-2, 4, hour_deg, int(self.h / 2 * 0.3), lcd.BLACK)
        # 分針の描画(2本線)
        needle(-1, 2, minute_deg, int(self.h / 2 * 0.45), lcd.BLACK)
        # 秒針の描画(1本線)
        needle(0, 1, self.second_deg, int(self.h / 2 * 0.6), lcd.RED)
        # 中心に赤丸
        lcd.circle(self.center_x, self.center_y, 3, lcd.RED, lcd.RED)


def env_meter_update():
    # M5 UI.Flow からの実行する場合は global 宣言が必要
    # APP.LIST に登録して実行する場合は、この global 宣言は不要
    global dht12, bmp280, t_meter, h_meter, p_meter, clock

    while True:
        # 次の秒までの差分(ミリ秒)を求めてスリープ
        time.sleep_ms(1000 - int(time.time() % 1 * 1000))

        # 時計の表示を更新
        clock.plot_needle()

        try:
            # DHT12 から湿度を取得
            dht12.measure()
            h = dht12.humidity()

            # BMP280 から気温、気圧を取得
            t, p = bmp280.values

            # それぞれのメーターに取得値を表示
            t_meter.plot_needle(t)
            h_meter.plot_needle(h)
            p_meter.plot_needle(p)
        except Exception as e:
            print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), ' Script Name: ', __name__)
            print('Exception: ', e)

        gc.collect()


# M5 UI.Flow からの実行ではなく、APP.LIST に登録して実行する場合は、
# プログラム内でネットワーク接続を行う必要がある。
# Connect network
import wifisetup
wifisetup.auto_connect()

# 日本時間に同期
rtc = machine.RTC()
rtc.ntp_sync('ntp.nict.jp', tz='JST-9')
# M5GOのfirmwareがv0.11ではntp_syncでtzを指定するとエラーになるので以下で対応
# rtc.ntp_sync('ntp.nict.jp')
# sys.tz('JST-9')

# 同期が完了するまで100ms程度かかる
for i in range(100):
    if rtc.synced():
        print('synced.')
        break
    print(i, end=' ')
    time.sleep_ms(10)

print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), ' Script Name: ', __name__)

lcd.setColor(lcd.BLACK, lcd.WHITE)
lcd.setTextColor(lcd.BLACK, lcd.WHITE)
lcd.clear(lcd.BLACK)

win_w, win_h = lcd.winsize() # (320, 240)

# 画面を4分割して、気温計、湿度計、気圧計、時計を表示
# 表示フォーマットは、表示桁数が減った時に(気圧が1000から999になった時)
# 前の表示を消すために前後に空白を入れている
t_meter = Meter(0,          0,          win_w // 2, win_h // 2,   0,   40, lcd.ORANGE, ' {:.1f}C ')
h_meter = Meter(win_w // 2, 0,          win_w // 2, win_h // 2,  20,  100, lcd.CYAN,   ' {:.1f}% ')
p_meter = Meter(0,          win_h // 2, win_w // 2, win_h // 2, 960, 1040, lcd.YELLOW, ' {:.1f}hPa ')
clock   = Clock(win_w // 2, win_h // 2, win_w // 2, win_h // 2, lcd.GREENYELLOW)

i2c = i2c_bus.get(i2c_bus.M_BUS)
dht12 = DHT12(i2c)
bmp280 = BMP280(i2c)

env_meter_update()

補足

以下は M5GO Cloud (io.m5go.com Firmware 0.15) で実行した場合の動作でしたが、 UI.Flow の Firmware ではプログラムは MainThread で動くので「stack overflow」で落ちることは(多分)ありません。

上記プログラムで一応動きますが、時間が経つと(今日動かしたときは半日ほどして)何が原因なのかはよくわかりませんが、以下のように「stack overflow」で落ちることがあります。

***ERROR*** A stack overflow in task m5go_run has been detected.
abort() was called at PC 0x4009230c on core 1

「stack overflow」を回避する代替案として以下のような方法が考えられますが、1./2. で長時間(何日も動かしても)問題ないかは確認したことなく、ちゃんと動かすなら 3. の方法を考えた方がいいように思います。

  1. Stack Sizeを大きくして別スレッドで動かす(M5GO Cloudから動かした場合はStack Sizeが4096のスレッドで動く)
  2. Timer起動でメインスレッドで動かす
  3. M5GO Cloudから動かすのをやめ、boot.py/main.py を置き換えて直接動かす