知的好奇心 for IoT

IoT関連の知的好奇心を探求するブログです

M5StickCと非接触温度センサHat(MLX90614搭載)でお手軽非接触温度計を作った

新型コロナウイルスが猛威を振るっている武漢で、かざすだけで(非接触で)体温が分かる装置を使って建物に入る前に体温を検査しているビデオを見ました。その時は特に注目もしていなかったのですが、たまたまスイッチサイエンスのサイトでM5StickC用の非接触温度センサHatを見つけて、「ああ、こんなのを使っていたんだ!」って興味が湧いたんです。

しかも、この製品の公開日が「2020年2月12日」で見つけた日は13日で出たばっかり!さらに、お値段も1,200円程度でとても安かったんです。

「これは買うしかない!」と思って、さっそく秋葉原千石電商やマルツに行ってみたんですが、どこにも置いてなかったんです。仕方なく部屋に戻って製品ぺージを見たら、「在庫多数」と表示されていたハズの在庫が「7個」ぐらいになっていたんです。あせりました。

スイッチサイエンスのサイトでは既に在庫「0」で次回入荷が「未定」となっていましたが、この記事を書いている17日の昼は千石電商の店頭に並んでいるのを見かけたので、欲しい方はダッシュでお店に行きましょう!

 

お手軽非接触温度計の写真

M5StickCに非接触温度センサHatを付けるとこんな感じです。とっても簡単で、非常にコンパクトです!

f:id:IntellectualCuriosity:20200218022339j:plain

でも、非接触温度センサHatは下に付いているピンだけで支えているために、本体との上の接触部分に隙間ができて少しぐらぐらするんです。

2020年3月2日修正

そのため、下の写真のように赤枠部分に両面テープを貼って本体とピッタリ付くようにしました。

f:id:IntellectualCuriosity:20200218023637p:plain

赤枠の長方形の窪みはなんだろうと思っていたら、箱の中に長方形の窪みにピッタリ合う切り込み付きの両面テープが入っていました。これで留めるのだと思います。

f:id:IntellectualCuriosity:20200302131605p:plain

M5StickCは小さいですがバッテリーを内蔵しているので、少しの間であればこれだけで色々なもの温度を測ることができるんですよ。

 

お手軽非接触温度計のビデオ

動作時のビデオを撮ったので、まずはご覧ください。

センサーを向けた物体表面の温度を表示する単純なものですが、スマホのようにM5StickCの向きに応じて表示が回転するようにしました。

また、Aボタンに押した時の画面に固定されるスクリーンロック機能と、Bボタンにローテーションを禁止するローテーションロック機能を付けています。

 

プログラム

UIFlowを使ってPythonでプログラムを書きたいと思っているのですが、使い慣れているArduinoでまずは作って見ることにしました。

「M5StickC」ライブラリの「NCIR_HAT」スケッチ例をベースとして、表示のローテーション機能やローテーションのロック機能、スクリーンロック機能などを付け加えています。

M5StickC_NCIR.ino

/*
 * NCIR Temperature Sensor
 * February 17, 2020
 * By Hiroyuki ITO
 * https://intellectualcuriosity.hatenablog.com/  
 * MIT Licensed.
 */
#include <M5StickC.h>
#include <Wire.h>

// Screen Orientation
#define TOP_LEFT    1
#define TOP_RIGHT   3

// Global Variables
int rotation = TOP_LEFT;        // Screen Orientation
boolean lock_rotation = false;  // Rotation Lock
boolean lock_screen = false;    // Screen Lock

// Rotate Screen
void rotated() {
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setRotation(rotation);
}

// Temperature Measurement
float measurement() {
  uint16_t result;

  Wire.beginTransmission(0x5A); // Send Initial Signal and I2C Bus Address
  Wire.write(0x07);             // Send data only once and add one address automatically.
  Wire.endTransmission(false);  // Stop signal
  Wire.requestFrom(0x5A, 2);    // Get 2 consecutive data from 0x5A, and the data is stored only.
  result = Wire.read();         // Receive DATA
  result |= Wire.read() << 8;   // Receive DATA
  
  return result * 0.02 - 273.15;
}

void setup() {
  // Initialize M5StickC
  M5.begin();
  M5.MPU6886.Init();              // Initialize 6-Axis IMU
  M5.Lcd.setRotation(rotation);   // Initialize Screen
  M5.Lcd.setTextColor(WHITE, BLACK);

  // Initialize I2C PIN for NCIR MLX90614 hat
  Wire.begin(0, 26);
}

void loop() {
  M5.update();

  // Toggle Screen Lock
  if (M5.BtnA.wasPressed()) {
    if (lock_screen) {
      lock_screen = false;
    } else {
      lock_screen = true;
    }
  }

  // Toggle Rotation Lock
  if (M5.BtnB.wasPressed()) {
    if (lock_rotation) {
      lock_rotation = false;
    } else {
      lock_rotation = true;
    }
  }

  if (lock_screen) {
    M5.Lcd.drawString("Screen Lock", 4, 2, 1);
  } else {
    M5.Lcd.drawString("           ", 4, 2, 1);

    if (lock_rotation) {
      M5.Lcd.drawString("Rotation Lock", 4, 70, 1);
    } else {
      M5.Lcd.drawString("             ", 4, 70, 1);
   
      // Change screen orientation with 6-axis IMU
      float accX = 0; float accY = 0; float accZ = 0;
      M5.MPU6886.getAccelData(&accX, &accY, &accZ);
      //Serial.println(accX);
      if (accX < 0 && rotation == TOP_LEFT) {
        rotation = TOP_RIGHT;
        rotated();
      }
      if (accX > 0 && rotation == TOP_RIGHT) {
        rotation = TOP_LEFT;
        rotated();
      }
    }

    // Temperature measurement and display
    float temp = measurement();
    String strTemp = String(temp, 1);
    if (-10 < temp && temp < 10) {
      strTemp = "   " + strTemp;
    }
    if (10 < temp && temp < 100) {
       strTemp = " " + strTemp;
    }
    M5.Lcd.drawRightString(strTemp, 150, 16, 7);
    //Serial.println(strTemp);
  }

  delay(100);
}

温度の取得(26行目から38行目)

接触温度測定ができるMLX90614赤外線温度センサーはI2Cで温度を取得することができます。

Adafruitなどからライブラリが提供されていますが、M5StickCのスケッチ例「NCIR_HAT」はライブラリを使わずに直接I2Cを扱うようになっていたので、温度を取得するコードを「measurement」ファンクションにまとめてそのまま使いました。

すごく短いコードなので理解しやすいと思います。

MLX90614のI2Cアドレス「0x5A」に「0x07」を送信した後、「0x5A」アドレスから2バイト受信した値に係数0.02を掛けて絶対零度273.15を引くと摂氏温度になるようです。

 

画面のローテーション制御

慣性センサーのMPU6886は「M5StickC」ライブラリから使えるようになっていて、M5.MPU6886.Init()で初期化した後はM5.MPU6886.getAccelData(&accX, &accY, &accZ)でX軸・Y軸・Z軸の加速度が取得できます。

M5StickCの横向きの加速度はX軸で取得できるので、X軸が0より大きかったら上側が左向き、X軸が0より小さかったら上側が右向きで文字が読めるように画面をローテイションさせています。

また、実際のローテーションは毎回のループで行うのではなく、上下の向きの変化があったときだけ行うようにしています。

 

文字表示に関するチョットしたこと

スケッチ例は文字色だけで文字の背景色を指定せずに表示を行っています。

でも、それだと同じ場所に文字を表示すると、前の文字が消えずに上書きしてしまうため文字が読めなくなってしまいます。そのため、文字を表示する前に背景色の長方形で塗りつぶしてから、新しい文字を表示するようなコードになっていました。

でも、そのコードだと文字がチラついて表示されるんです。

そのため、文字の背景色を黒にして、いちいち文字を表示するまえに表示する部分を長方形で塗りつぶさなくてもよくして、表示がちらつかないようにしました。

しかし、その弊害がコードの99行目から104行目に出ています。

これは、表示桁数が減った時にマイナス表示や前の数値が残ってしまうのを防ぐために、空白で背景色だけの文字を表示するためのコードなんです。

 

UIFlowのBlocklyで作ってたらこうなった

M5Stackが用意しているブロックプログラミング環境UIFlowのBlocklyで同様のプログラムを作ってみました。(図をクリックすると拡大表示します)

f:id:IntellectualCuriosity:20200220115004p:plain

しかし、緑色の「画面の向きのモードを[]に設定する」ブロックが正常に動作しなくて、M5StickCをX軸方向に傾けても画面のローテーションが行われませんでした。

また、使用できる文字のフォントセットがArduinoと違っていて、Arduinoで使用していたカッコいい「48ピクセル7セグ風フォント」を使うことができませんでした。

f:id:IntellectualCuriosity:20200220122816j:plain

作られたPythonコード

Blocklyで作ったプログラムはPythonでも見ることができます。

それで、Pythonのコードを確認したら、57行目と59行目の後にあるはずの画面をローテーションするコードlcd.setRotation()がありませんでした。

コードがなければ動作する訳はないですよね。

from m5stack import *
from m5ui import *
from uiflow import *
lcd.setRotation(0)
import imu
import hat
lcd.setRotation(2)
import hat

setScreenColor(0x000000)

hat_ncir0 = hat.get(hat.NCIR)

imu0 = imu.IMU()
label_temperature = M5TextBox(59, 46, "Temp", lcd.FONT_DejaVu40,0xffffff, rotate=90)
label_screen = M5TextBox(79, 5, "Screen Lock", lcd.FONT_Default,0xffffff, rotate=90)
label_rotation = M5TextBox(15, 5, "Rotation Lock", lcd.FONT_Default,0xffffff, rotate=90)

rotation = None
lock_screen = None
lock_rotation = None
accX = None

def buttonA_wasPressed():
  global rotation, lock_screen, lock_rotation, accX
  if lock_screen:
    lock_screen = False
    label_screen.hide()
  else:
    lock_screen = True
    label_screen.show()
  pass
btnA.wasPressed(buttonA_wasPressed)

def buttonB_wasPressed():
  global rotation, lock_screen, lock_rotation, accX
  if not lock_screen:
    if lock_rotation:
      lock_rotation = False
      label_rotation.hide()
    else:
      lock_rotation = True
      label_rotation.show()
  pass
btnB.wasPressed(buttonB_wasPressed)

rotation = 'TOP_LEFT'
lock_rotation = False
lock_screen = False
label_rotation.hide()
label_screen.hide()
while True:
  if not lock_screen:
    if not lock_rotation:
      accX = imu0.acceleration[0]
      if accX < 0 and rotation == 'TOP_LEFT':
        rotation = 'TOP_RIGHT'
      if accX > 0 and rotation == 'TOP_RIGHT':
        rotation = 'TOP_LEFT'
    label_temperature.setText(str("%.1f"%float((hat_ncir0.temperature))))
  wait_ms(100)
  wait_ms(2)

よくよくPythonのコードを見ると、最終行になぜかBlocklyにはないwait_ms(2)があったりとちょこちょこおかしなところがあります。

今回使用したUIFlowのバージョンはV1.4.4ですが、まだ少し問題が残っているようです。

でも、UIFlowで大まかなプログラムを組んでから、作られたPythonコードを修正する方が、最初からPythonでコードを書くより早いと感じました。

特に非接触温度センサーや慣性センサーなどのデバイスの使い方を調べる必要がないのはいいです!