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

ham-tacho-graph

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

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

うさたん

わぁ グラフ きれい!

まあちゃん

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

これまで紹介したフォトリフレクタWebIOPiの使い方の記事はこちらです。

目次

ハムスターの紹介

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

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

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

うさたん

むっちゃ、かわいいです

まあちゃん

ほんと、癒されます

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

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

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

hamster

回転計の作成にあたって、こちらのブログを参考にさせていただきました。ありがとうざいました!

ハムスターの回転計(回し車 カウンター) [光学]

準備するもの

1ラズベリーパイ(Raspberry pi zero WH)
詳しくは、こちらです。
初期設定環境設定が済んだものを使用します。
Raspberry-Pi-Zero-WH
2microSDカード
8~32GB
詳しくは、こちらです。
Team-microSDHC-Card
3microUSB電源ケーブル
スマホの充電ケーブルでOKです。
詳しくは、こちらです。
microUSB-power-cable
4パソコン
microSDに書き込めるもの。
詳しくは、こちらです。
UX331UN-8250B
5ブレッドボード
詳しくは、こちらです。
bread-board
6ジャンパーワイヤー
詳しくは、こちらです。
Jumper-wire
7抵抗
詳しくは、こちらです。
electrical-resistance
8フォトリフレクタ(反射型フォトセンサ)
詳しくは、こちらです。
photo-reflector
98ビットシフトレジスタ 74HC595
詳しくは、こちらです。
74HC595
10温湿度センサ DHT11
詳しくは、こちらです。
DHT11
11ハムスターの回し車
一部に赤いピカピカのテープを貼りました。
回し車が回転すると、フォトリフレクタでこのテープの通過を検出します。
ham-turn-car
12ブレッドボードを固定する台
固定できれば何でもOKです。
次で詳しく紹介しています。 
raspi-lego

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

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

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

raspi-lego

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

raspi-lego2

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

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

raspi-lego3

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

raspi-lego4

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

raspi-lego5

配線方法

回路図

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

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

circuit-diagram

配線図

配線の方法を、簡単な図で示します。

ham-tacho-wiring

配線の写真

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

ham-tacho-connection

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

ham-tacho-connection2

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

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

いろいろな種類のグラフを簡単に表示できるライブラリー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

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

chart-js2

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

chart-js3

/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ファイルから、日付選択のセレクトボックスを生成します。

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

ファイル構成

ファイルの構成は、こちらです。

file-configuration

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/にアクセスします。

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

hamster

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

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

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

ham-tacho-graph

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

まとめ

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

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

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

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

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

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

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

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次