ラズベリーパイ(Raspberry Pi Zero WH)と、これまで紹介したフォトリフレクタ、WebIOPiを使って、ハムスターの回し車の回転計を作成します。
測定した回転数は、JavaScriptのチャートライブラリ「Chart.js」を使ってグラフ表示し、リアルタイムでパソコンやスマホから確認することができます。
わぁ グラフ きれい!
Chart.jsを使うと簡単に
グラフが作れるんだよ
これまで紹介したフォトリフレクタとWebIOPiの使い方の記事はこちらです。
ハムスターの紹介
こちらが我が家のプリンセス! ジャンガリアンハムスターの「ハムハム」です。
しぐさがとっても愛らしくて、手を入れるとすぐに寄ってくる人懐っこい女の子です。今、生後8カ月くらいです。
むっちゃ、かわいいです
ほんと、癒されます
でも、かわいがりすぎて、最近ちょっと太め? ダイエットが必要でしょうか?
夜、結構、回し車を回しているようなのですが、実際どのくらい回しているのか、調べてみたいと思います。
これまで紹介したフォトリフレクタとWebIOPiのしくみを利用して、ラズベリーパイ(Raspberry Pi Zero WH)でハムスターの回転計を作成します。
回転計の作成にあたって、こちらのブログを参考にさせていただきました。ありがとうざいました!
準備するもの
1 | ラズベリーパイ(Raspberry pi zero WH) 詳しくは、こちらです。 初期設定・環境設定が済んだものを使用します。 | |
2 | microSDカード 8~32GB 詳しくは、こちらです。 | |
3 | microUSB電源ケーブル スマホの充電ケーブルでOKです。 詳しくは、こちらです。 | |
4 | パソコン microSDに書き込めるもの。 詳しくは、こちらです。 | |
5 | ブレッドボード 詳しくは、こちらです。 | |
6 | ジャンパーワイヤー 詳しくは、こちらです。 | |
7 | 抵抗 詳しくは、こちらです。 | |
8 | フォトリフレクタ(反射型フォトセンサ) 詳しくは、こちらです。 | |
9 | 8ビットシフトレジスタ 74HC595 詳しくは、こちらです。 | |
10 | 温湿度センサ DHT11 詳しくは、こちらです。 | |
11 | ハムスターの回し車 一部に赤いピカピカのテープを貼りました。 回し車が回転すると、フォトリフレクタでこのテープの通過を検出します。 | |
12 | ブレッドボードを固定する台 固定できれば何でもOKです。 次で詳しく紹介しています。 |
ブレッドボードを固定する台
フォトリフレクタの高さを調節して、ブレッドボードを固定する台を作成しました。
ブレッドボードの後ろにも両面テープが貼ってあるようなので、空き箱を利用して作ろうかと思っていたのですが、子どもが小さいころ遊んでいたレゴブロックのドアがちょうどいいサイズ!
こちらは、後ろからの写真です。ブレッドボードの横にちょっと隙間があくので、ジャンパーワイヤを通すことができました。
横にラズベリーパイ(Raspberry Pi Zero WH)を置いて、赤い柵で固定しています。
ちょうどラズベリーパイ(Raspberry Pi Zero WH)の電源ケーブルが赤い柵の隙間に固定されていいかんじです。
この台を、ハムスターの回し車のところにおいて、回転数を数えます。
横から見るとこんなかんじです。フォトリフレクタを回し車の近くにおいて、赤いピカピカテープが通過すると数をカウントしていきます。
配線方法
回路図
回路図はこのようにしました。
温湿度センサーのDHT11は、4.7kΩのプルアップ抵抗が必要ということで、手元にあった10kΩの抵抗を使用しました。
配線図
配線の方法を、簡単な図で示します。
配線の写真
写真で見るとこんなかんじです。
ブレッドボードの方をアップにします。
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」ボタンをクリックします。
InstallationのGitHub releasesリンクをクリックします。
ページの一番下のChart.jsファイルをクリックしてダウンロードします。
/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
使い方
スケジューラを使う方法は、
- プログラム内でインポート
- 実行したいスケジュールと関数を登録
- 登録した関数を実行する
という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
使い方
このモジュールを使う方法は、こちらです。
- プログラム内でインポート
- 使用するGPIOを指定
- 温度・湿度を読み取る
詳しくは、プログラムを見てください。
import Adafruit_DHT as DHT # DHT11用ライブラリ
……
DHT_PIN = 15 # 温湿度入力ピン GPIO15(ピン番号10)
……
humi, temp = DHT.read_retry(DHT.DHT11, DHT_PIN)
プログラム
プログラムは、大きく2つの流れで処理しています。
- 回転数をCSVファイルに保存
- 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万回くらいの日もあります。
年齢や季節、温度にもよるようですし、運動量で体調が分かって、エサの量を調節することもできそうです。
もしかしたら地震の予知ができるかもしれません。
次は、スマホからロボットをラジコン操作したいと思います。