ラズベリーパイ(Raspberry Pi Zero WH)とフォトリフレクタでハムスターの回転計

2019/04/26

t f B! P L
ラズベリーパイ(Raspberry Pi Zero WH)と、これまで紹介したフォトリフレクタWebIOPiを使って、ハムスターの回し車の回転計を作成します。

測定した回転数は、JavaScriptのチャートライブラリ「Chart.js」を使ってグラフ表示し、リアルタイムでパソコンやスマホから確認することができます。

ハムスター回し車の回転数グラフ



まあちゃん
わぁ グラフ きれい!



ままあちゃん
Chart.jsを使うと簡単に
グラフが作れるんだよ





これまで紹介したフォトリフレクタWebIOPiの使い方の記事はこちらです。
ラズベリーパイ(Raspberry Pi Zero WH)でのフォトリフレクタ(フォトセンサ)の使い方
ラズベリーパイ(Raspberry Pi Zero WH)をスマホから操作する方法(WebIOPi)

ハムスターの紹介

こちらが我が家のプリンセス! ジャンガリアンハムスターの「ハムハム」です。

ジャンガリアンハムスター

2018年の8月末くらいに生まれたそうなので、生後8カ月くらいです。

しぐさがとっても愛らしくて、手を入れるとすぐに寄ってくる人懐っこい女の子です。


うさたん
むっちゃ、かわいいです



ままあちゃん
ほんと、癒されます




でも、かわいがりすぎて、最近ちょっと太め? ダイエットが必要でしょうか?

夜、結構、回し車を回しているようなのですが、実際どのくらい回しているのか、調べてみたいと思います。

これまで紹介したフォトリフレクタWebIOPiのしくみを利用して、ラズベリーパイ(Raspberry Pi Zero WH)でハムスターの回転計を作成します。

ジャンガリアンハムスター

回転計の作成にあたって、こちらのブログを参考にさせていただきました。ありがとうざいました!
ハムスターの回転計(回し車 カウンター) [光学]

準備するもの

1 ラズベリーパイ(Raspberry pi zero WH)
詳しくは、こちらです。
初期設定環境設定が済んだものを使用します。
Raspberry Pi Zero WH
2 microSDカード 8~32GB
詳しくは、こちらです。
Team microSDHCカード 32GB 高速転送UHS-1 日本国内10年保証 SD変換アダプター付属 正規品
3microUSB電源ケーブル
スマホの充電ケーブルでOKです。
詳しくは、こちらです。
microUSB電源ケーブル
4 パソコン
microSDに書き込めるもの。
詳しくは、こちらです。
エイスース 13.3型ノートパソコン ASUS ZenBook UX331UN ロイヤルブルー UX331UN-8250B

5 ブレッドボード
詳しくは、こちらです。
ブレッドボード
6 ジャンパーワイヤー
ジャンパーワイヤ(オスーメス)を2本使用します。詳しくは、こちらです。
ジャンパーワイヤ
7 抵抗
今回は、330Ωの抵抗を1本使います。詳しくは、こちらです。
抵抗
8 フォトリフレクタ(反射型フォトセンサ)
ローム株式会社(ROHM)のフォトリフレクタ(反射型フォトセンサ)RPR-220をベースに、この後詳しく説明します。
フォトリフレクタ
9 74HC14
詳しくは、こちらです。
74HC14
10 温湿度センサ DHT11
詳しくは、こちらです。
DHT11
11 ハムスターの回し車
一部に赤いピカピカのテープを貼りました。
回し車が回転すると、フォトリフレクタでこのテープの通過を検出します。
テープを貼ったハムスターの回し車
11 ブレッドボードを固定する台
固定できれば何でもOKです。
次で詳しく紹介しています。 
レゴでラズベリーパイの台

ブレッドボードを固定する台

フォトリフレクタの高さを調節して、ブレッドボードを固定する台を作成しました。

ブレッドボードの後ろにも両面テープが貼ってあるようなので、空き箱を利用して作ろうかと思っていたのですが、子どもが小さいころ遊んでいたレゴブロックのドアがちょうどいいサイズ!

レゴでラズベリーパイの台

こちらは、後ろからの写真です。ブレッドボードの横にちょっと隙間があくので、ジャンパーワイヤを通すことができました。

レゴでラズベリーパイの台


横にラズベリーパイ(Raspberry Pi Zero WH)を置いて、赤い柵で固定しています。

ちょうどラズベリーパイ(Raspberry Pi Zero WH)の電源ケーブルが赤い柵の隙間に固定されていいかんじです。

レゴでラズベリーパイの台


この台を、ハムスターの回し車のところにおいて、回転数を数えます。

レゴでラズベリーパイの台


横から見るとこんなかんじです。フォトリフレクタを回し車の近くにおいて、赤いピカピカテープが通過すると数をカウントしていきます。

レゴでラズベリーパイの台

配線方法

回路図

回路図はこのようにしました。

温湿度センサーのDHT11は、4.7kΩのプルアップ抵抗が必要ということで、手元にあった10kΩの抵抗を使用しました。
回転計の回路図

配線図

配線の方法を、簡単な図で示します。
ハムスターの回転計の配線

配線の写真

写真で見るとこんなかんじです。

Raspberry Pi Zero WHと回転計の配線写真


ブレッドボードの方をアップにします。

回転計の配線写真

JavaScriptのチャートライブラリー(Chart.js)

次は、ラズベリーパイに必要なライブラリーを追加していきます。

いろいろな種類のグラフを簡単に表示できるライブラリーChart.jsを使います。

Chart.jsは、HTML5のcanvasを使って、JavaScriptベースでグラフを表示することができるライブラリです。

公式サイトなどで使い方や設定などが、詳しく説明されています。

設定方法は、以下の2つがあります。

設定方法1 ライブラリにアクセス

外部のライブラリにアクセスするのであれば、htmlファイルのheadタグに以下を記述するだけでOKです。
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.1.4/Chart.min.js"></script>

設定方法2 ダウンロードする

外部にはアクセスしないで、ダウンロードしたファイルを使うこともできます。

でも、その方法がなかなか見つからなかったので、設定方法を紹介します。

公式サイトにアクセスし、「Git Started」ボタンをクリックします。

chart.jsのダウンロード1


InstallationのGitHub releasesリンクをクリックします。

chart.jsのダウンロード2


ページの一番下のChart.jsファイルをクリックしてダウンロードします。

chart.jsのダウンロード3

/home/pi/webiopi/test/jsフォルダを作成し、ダウンロードしたChart.jsファイルを保存します。

htmlファイルのheadタグでChart.jsを読み込みます。
<!-- グラフ描画用のチャートライブラリファイル読み込み -->

<script src="js/Chart.js"></script>

グラフのデータ・オプションを設定

javascriptで、描画するグラフのデータやオプションを設定することができます。(詳しくは、プログラムを見てください)
// グラフのデータ・オプションを設定

var ctx = $('#canvas').get(0);

var myChart = new Chart(ctx, {

  type: 'bar', // 棒グラフを指定

  data: {

    labels: array_lavels, // 横軸のラベル

    datasets: [{

      data: array_data, // グラフのデータ

      backgroundColor: 'rgba(153, 102, 255, 0.2)', // 棒グラフの背景色

      borderColor: 'rgba(153, 102, 255, 1)', // 棒グラフの縁取りの色

      borderWidth: 1  // 棒グラフの縁取りの太さ

             ……

キャンバスでグラフ描画

あとは、bodyタグ内のグラフを描画したい場所に「canvasタグ」をおくだけで、きれいなグラフが描けてしまいます。とっても簡単で楽しいです。
<!-- HTML5のcanvasで、グラフを描画 -->

<canvas id="canvas"></canvas>

Pythonのスケジューラーモジュール

もう一つ、Pythonのプログラムで「5分ごとに処理を実行」したいので、スケジューラーモジュールをインストールします。

インストール

まずは、現在インストール済みのパッケージを一覧で確認します。
> pip3 list
pip3は、Python3のパッケージを管理するコマンドです。
しばらくすると、インストール済みのパッケージがずらっと表示されます。

schedule」がなければpip3でインストールします。
> sudo pip3 install schedule

使い方

スケジューラを使う方法は、
  1. プログラム内でインポート
  2. 実行したいスケジュールと関数を登録
  3. 登録した関数を実行する
という3段階攻撃です(詳しくは、プログラムを見てください)。
import schedule              # スケジューラライブラリ

……

# 5分毎にseveCSVを実行するようスケジュールを登録

schedule.every(5).minutes.do(seveCSV)

……

# 設定したスケジュール通り、5分毎にseveCSV関数を実行

schedule.run_pending()

PythonのDHT11モジュール

温湿度センサーDHT11を簡単に使用するために、Pythonのモジュールを使用します。

インストール

Adafruit社がGitHubで公開しているPythonモジュールをインストールします。
> git clone https://github.com/adafruit/Adafruit_Python_DHT.git
> cd Adafruit_Python_DHT
> sudo python3 setup.py install

使い方

このモジュールを使う方法は、こちらです。
  1. プログラム内でインポート
  2. 使用するGPIOを指定
  3. 温度・湿度を読み取る

詳しくは、プログラムを見てください。
import Adafruit_DHT as DHT # DHT11用ライブラリ

……

DHT_PIN = 15 # 温湿度入力ピン GPIO15(ピン番号10)

……

humi, temp = DHT.read_retry(DHT.DHT11, DHT_PIN)

プログラム

プログラムは、大きく2つの流れで処理しています。
  1. 回転数をCSVファイルに保存
  2. CSVファイルを読み込んでグラフ描画

回転数をCSVファイルに保存

フォトリフレクタからの信号をカウントした回転数と、DHT11で測定した温度湿度を一定時間ごとにCSVファイルに保存します。

ハムスターは夜行性なので、夜20時くらいから、翌朝の6時くらいまでの回転数を測定します。

夜間の回転数を続けて見たいので、昼間の12時から24時間の単位で区切って保存することにしました。

CSVファイルを読み込んでグラフ描画

保存してあるCSVファイルのうち最新のファイルを読み込み、グラフを描画します。

保存されているCSVファイルから、日付選択のセレクトボックスを生成します。

セレクトボックスで日付を選択すると、グラフのデータを切り替えて再表示します。

ファイル構成

ファイルの構成は、こちらです。
ファイル構成
JavascriptやCSS(スタイルシート)などは、別のファイルにした方がいいのですが、ここでは内容を追いやすいように1つのファイルにまとめています。

index.html

index.htmlファイルでは、データのグラフ表示と回し車の状態・回転数を表示します。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Raspberry Pi Zero WH | ハムスターの回転数</title>
<!-- WebIOPiのJavascriptライブラリファイル読み込み -->
<script type="text/javascript" src="/webiopi.js"></script>
<!-- グラフ描画用のチャートライブラリファイル読み込み -->
<script src="js/Chart.js"></script>
<script type="text/javascript">
// WebIOPiの準備が整ったときに呼び出す関数
webiopi().ready(function() {

  // 表示データ設定関数の定義
  var setDispData = function(macro, args, response){

    // getDispDataマクロからの戻り値を取得
    var getData = response.split(";");

    // グラフ横軸のラベル(時間)
    var array_lavels = getData[0].split(',');

    // グラフのデータ(回転数)
    var array_data   = getData[1].split(',');

    // グラフのデータ(温度)
    var array_temp   = getData[2].split(',');

    // グラフのデータ(湿度)
    var array_humi   = getData[3].split(',');

    // セレクトボックスにoption要素がなければ生成
    if( !($('#filelist').children().length) ){

      // ファイルリスト取得
      var array_files  = getData[4].split(',');
  
      // ファイルリストをセレクトボックスに設定
      for(let i = 0; i < array_files.length; i++){
        $('#filelist').append('<option value="'+i+'">'+array_files[i]+'</option>');     
      }
    }

    // 合計数を#totalに設定
    $("#total").text(getData[5]);

    // グラフのデータ・オプションを設定
    var ctx = $('#canvas').get(0);
    var myChart = new Chart(ctx, {
      type: 'bar',                                      // 棒グラフを指定
      data: {
        labels: array_lavels,                           // 横軸のラベル
        datasets: [
        {
          label:'回転数',
          data:            array_data,                  // グラフのデータ
          backgroundColor: 'rgba(153, 102, 255, 0.2)',  // 棒グラフの背景の色
          borderColor:     'rgba(153, 102, 255, 1)',    // 棒グラフの縁取りの色
          borderWidth:     1,                          // 棒グラフの縁取りの太さ
          yAxisID:'y-left'
        },
        {
          type:'line',
          label:'温度',
          data:            array_temp,                  // グラフのデータ
          backgroundColor: 'rgba(255, 99, 132, 0.2)',   // 棒グラフの背景の色
          borderColor:     'rgba(255,99,132,1)',        // 棒グラフの縁取りの色
          borderWidth:     1,                          // 棒グラフの縁取りの太さ
          fill: false,                                  // 折れ線グラフを中抜きに
          yAxisID:'y-right'
        },
        {
          type:'line',
          label:'湿度',
          data:            array_humi,                  // グラフのデータ
          backgroundColor: 'rgba(54, 162, 235, 0.2)',   // 棒グラフの背景の色
          borderColor:     'rgba(54, 162, 235, 1)',     // 棒グラフの縁取りの色
          borderWidth:     1,                          // 棒グラフの縁取りの太さ
          fill: false,                                  // 折れ線グラフを中抜きに
          yAxisID:'y-right'
        }
        ]
      },
      options:{
        title: {                       // グラフタイトル
          display:  true,
          fontSize: 18,
          text:     '回し車の回転数'
        },
        scales: { 
          yAxes: [{
            id: "y-left",               // Y軸のID
            type: "linear",             // linear固定 
            position: "left",           // 表示位置
            ticks: {
              beginAtZero: true         // 縦軸の座標を0から始める
            }
          },
          {
            id: "y-right",              // Y軸のID
            type: "linear",             // linear固定 
            position: "right",          // 表示位置
            ticks: {
              max: 90,
              min: 0
            },
            gridLines: {                 // 横グリッド線を非表示
              drawOnChartArea: false, 
            }
          }
          ]
        }
      }
    });
  }


  // データ表示関数の定義
  var dispData = function(drawData){

    // getDispDataマクロを呼び出した後、setDispData関数を実行
    webiopi().callMacro("getDispData", [drawData], setDispData);

  }

  // カウンター更新関数の定義
  var updateCounter = function(macro, args, response) {

    // getCounterマクロで取得したカウント数が0より大きかったら
    if ( response  ){

      // getCounterマクロで取得したカウント数を#counterに設定
      $("#counter").text(response);

    }
  }

  // カウンターチェック関数の定義
  var checkCount = function(){

    // getCounterマクロを呼び出した後、updateCounter関数を実行
    webiopi().callMacro("getCounter", [], updateCounter);

    // 1秒後にcheckCount関数を実行
    setTimeout(checkCount, 1000);

  }

  // グラフとデータを表示
  dispData(0);

  // GPIOボタンを作成
  var button = webiopi().createGPIOButton(17, "回転中");

  // #controlsにGPIOボタンを追加
  $("#controls").append(button);

  // カウンターチェック関数を実行
  checkCount();

  // Refresh GPIO buttons
  webiopi().refreshGPIO(true);

  // セレクトボックス変更時、選択された値を取得する
  $('[name=files]').change(function() {

    // グラフとデータを表示
    dispData($('[name=files]').val());

  });

});

</script>

<style type="text/css">
    button {
        display: inline-block;
        margin: 5px 5px 5px 5px;
        width: 80px;
        height: 35px;
        font-size: 12pt;
        color: white;
    }
                
    #gpio17.HIGH {
        background-color: Pink;
    }
                
    #gpio17.LOW {
        background-color: Red;
    }
</style>
</head>
<body>
<!-- HTML5のcanvasで、グラフを描画 -->
<canvas id="canvas"></canvas>

<!-- 日付選択 セレクトボックスと回転数合計 -->
<p><b>表示日</b>: <select id="filelist" name='files'></select>  <b>合計</b>: <span id="total">0</span>回</p>

<!-- GPIOボタンと現在の回転数 -->
<p><span id="controls"></span> <b>回転数</b>: <span id="counter">0</span>回</p>

</body>
</html>

script.py

実際に処理をしているPythonのソースファイルです。
import webiopi              # WebIOPiライブラリ
import datetime             # 日付時刻ライブラリ
import schedule             # スケジューラライブラリ
import csv                  # csvファイルライブラリ
from pathlib import Path    # ファイル関連のライブラリ
import Adafruit_DHT as DHT  # DHT11用ライブラリ

#debug
#webiopi.setDebug()
#webiopi.debug("デバッグメッセージ")

GPIO = webiopi.GPIO

LOOP_TIME = 0.1       # LOOPする時間の間隔(0.1秒)

IN_GPIO = 17          # GPIO pin using BCM numbering
                      # GPIO17(ピン番号11)

DHT_PIN = 15          # 温湿度入力ピン GPIO15(ピン番号10) 

PREV_GPIO = GPIO.LOW  # 前回のGPIO値

COUNTER = 0           # カウンター

PREV_COUNTER = 0      # 前回のカウンター値

                      # CSVファイルのPath
DATA_DIR = '/home/pi/webiopi/test/data/'

CSV_SAVE_TIME = 10    # CSVファイルに保存する間隔(10分)


# setup function is automatically called at WebIOPi startup
def setup():
    # set the GPIO used by the light to output
    GPIO.setFunction(IN_GPIO, GPIO.IN)

    #指定した時間ごとに、CSVファイルに回転数を保存するよう設定
    schedule.every(CSV_SAVE_TIME).minutes.do(seveCSV)

# loop function is repeatedly called by WebIOPi 
def loop():

    # 変更するグローバル変数を宣言
    global COUNTER, PREV_COUNTER, PREV_GPIO

    # GPIOの入力が、HIGHからLOWに変わったら
    if ( GPIO.digitalRead(IN_GPIO) == GPIO.LOW
     and PREV_GPIO == GPIO.HIGH ):
        COUNTER += 1
        PREV_GPIO = GPIO.LOW
    # GPIOの入力が、LOWからHIGHに変わったら
    elif ( GPIO.digitalRead(IN_GPIO) == GPIO.HIGH
     and   PREV_GPIO == GPIO.LOW ):
        PREV_GPIO = GPIO.HIGH

    # setup()で設定したスケジュール通りに処理を実行(CSV保存)
    schedule.run_pending()

    # LOOP_TIME時間 スリープしたあと、ループを繰り返す
    webiopi.sleep(LOOP_TIME)


# destroy function is called at WebIOPi shutdown
#def destroy():
#    GPIO.digitalWrite(IN_GPIO1, GPIO.LOW)



# CSVファイルにデータを保存する
# お昼の12時から24時間で1ファイルにする
def seveCSV():

    # 変更するグローバル変数を宣言
    global COUNTER

    # 現在時刻を取得
    now = datetime.datetime.now()

    # お昼の12時以降だったら
    if ( now.hour >= 12 ) :
        # 次の日の日付を織り込んだファイル名を生成
        file_name = DATA_DIR+"cnt_{0:%Y%m}{1}.csv".format(now,now.day+1) 
    else:
        # 日付を織り込んだファイル名を生成
        file_name = DATA_DIR+"cnt_{0:%Y%m%d}.csv".format(now)

    # 温度・湿度を取得
    for i in range(3):
        humi, temp = DHT.read_retry(DHT.DHT11, DHT_PIN)

        # 値が異常ならリトライ
        if (humi > 90) or (temp > 50):
            sleep(0.1)
            continue
        break

    # 書き込みデータ生成 HH:MM,回転数,温度,湿度 ex.10:25,120,22,66
    write_data = ["{0:%H:%M}".format(now), COUNTER,round(temp),round(humi)]
  
    # カウンターをリセット
    COUNTER = 0

    # データをCSVファイルに書き込み
    with open(file_name, 'a') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(write_data)


# グラフデータを取得するマクロ
@webiopi.macro
def getDispData(disp_data):

    # ファイル名を取得(PosixPathオブジェクト)
    file_list = sorted(Path(DATA_DIR).glob('*.csv'), reverse=True)

    # CSVファイルを読み込む
    with open(str(file_list[int(disp_data)])) as fp:
        reader = list(csv.reader(fp))
  
    # ファイルの日付部分のみの文字列のlist(配列)に変換(セレクトボックス用))
    str_files = []
    for row in file_list:
        str_files.append((str(row))[31:39])

    # 二次元配列の行と列を入れ替える
    reader = list(map(list, zip(*reader)))

    # 回転数の合計を計算する
    total = sum(int(i) for i in reader[1])

    # list(配列)を文字列に変換(前後の[]をカット)
    str_time = ','.join(reader[0])
    str_cnt  = ','.join(reader[1])
    str_temp = ','.join(reader[2])
    str_humi = ','.join(reader[3])
    str_file = ','.join(str_files)

    return  "%s;%s;%s;%s;%s;%d" % (str_time, str_cnt, str_temp, str_humi, str_file, total)


# カウントアップしていたら、カウンター値を、前回と同じなら0を返すマクロ
@webiopi.macro
def getCounter():

    # 変更するグローバル変数を宣言
    global PREV_COUNTER

    if ( PREV_COUNTER != COUNTER):
        PREV_COUNTER = COUNTER
        return "%d" % (COUNTER)
    return 0  

データファイル

プログラム内で作成し、データを保存するcsvファイルです。

ファイル名は、cnt_(日付).csvになります。

フォーマットは、時間,回転数,温度,湿度 です。
20:10,281,25,51
20:20,678,25,54
20:30,687,25,52
20:40,407,25,51
20:50,313,25,52
21:00,458,26,53
21:10,581,26,54

動作確認

WebIOPiを起動して、http://raspberrypi.local:8000/test/にアクセスします。

ハムハムが回し車を回すと……。

回し車をまわすハムハム


回転数グラフの下の「回転中」ボタンが赤くなって、回転数がアップしていきます。

保存されたデータのグラフが表示されます。

回転数は紫の棒グラフ、温度は赤、湿度は青の折れ線グラフです。

ハムスター回し車の回転数グラフ

終了するときは、WebIOPiを停止させ、ラズベリーパイを停止します。

まとめ

気になっていたハムハムの回し車の回転数を、実際に測定してグラフ化したことで、運動量が一目で分かるようになりました。

自動で夜間ずっと計測してくれる便利さも実感です。

測り始めてまだ数日ですが、日によって回転数にばらつきがあることも分かりました。

多い日には、ほとんど休みなく回して2万回以上! 休みが多くて1万回くらいの日もあります。

年齢や季節、温度にもよるようですし、運動量で体調が分かって、エサの量を調節することもできそうです。

もしかしたら地震の予知ができるかもしれません。

次は、スマホからロボットラジコン操作したいと思います。

『 ラズベリーパイ(Raspberry Pi Zero WH)でScratch ロボットと遊ぼう! 』で紹介したロボットを、今度は Python と Javascript で動かしてみたいと思います。 さらにロボットの前につけたカメラで、ロボットがみている風景を見ながら...

このブログを検索

プロフィール


こんにちは!
小学生の男の子 2人のママです。

ラズベリーパイを使った簡単な電子工作とプログラミングに挑戦しています。

よかったら、詳しいプロフィールも見てください。

QooQ