この記事は「もくもく会ブログリレー」12日目 の記事です。

先日ラズパイと重量センサを買って遊んでみたので、学んだことをまとめてみました。
今私が悩みを抱えているのが、猫のフードロスです。


左がキイちゃん。右がメラくん。

我が家の猫は、餌を一度に食べず、自由に少しずつ食べていくタイプです。
猫たちの食べるカリカリはしばらく放っておくと、湿気を吸い込んでふやけてしまいます。そうなると、猫たちは、「こんな湿気ったえさは食べないぞ」という感じで、湿気った餌を避けて食べます。
今は猫たちの餌を食べるタイミングが予測できず、どうしてもフードロスにつながってしまっているのが課題です。

そこで今回は、ラズパイ+重量センサーを使ってグラフ化し、ベストなタイミングと量を見つけたいと思います。
 

用意したもの

  • ラズベリーパイ4(ファン付き)…※長時間ラズパイを稼働させるので、途中アツアツになって、急いでファンを調達しました。
  • 重量センサ(デジタルロードセル 重量センサ5KG+HX711)
  • 金貨(代用可)
  • 猫が警戒しないようにラズパイを隠す入れ物(あれば)


 

手順

  • モジュールのインストール
  • ラズパイと重量センサの接続
  • キャリブレーション値の決定
  • 10分おきに重さを測定する
  • データを保存する
  • データ整形とグラフ化

 

モジュールのインストール

まず最初に必要なモジュールをインストールします

$ python -m venv env # 仮想環境の作成
$ source env/bin/actiavte # 仮想環境に入る

$ pip install RPi.GPIO # 重量センサで使用
$ git clone https://github.com/tatobari/hx711py # 重量センサで使用

$ pip install matplotlib # グラフ化で使用
$ pip intall japanize_matplotlib # グラフ化で使用

 

ラズパイと重量センサの接続

重量センサについている緑の部分がロードセル用ADコンバータです。VCC, SCK, DT, GNDと書かれたピンがついています。

これらにRaspberry Pi4のGPIOを接続しました。

VCC 5V (ピン番号2)
SCK GPIO6(ピン番号31)
DT GPIO5(ピン番号29)
GND GND(ピン番号39)

 

キャリブレーション値の決定

 キャリブレーションとは、測定器や機械を正確に動くように設定することです。例えば、はかりを使ってお菓子の重さを知りたい時、はかりに「どのくらいの重さで1グラムとするか」を設定します。これによって、正確な重さを計算できます。
 
キャリブレーション値を決めるために、手元に重さがあらかじめわかっているものを用意します。
今回は百円玉(1枚 4.8g ✖️ 10枚)を使いました。
これから「この重さは48gだよ。覚えてね」という設定をしていきます。

まずキャリブレーションの値を1と仮決めして、計測します

import sys
import RPi.GPIO as GPIO
from hx711py.hx711 import HX711

PIN_DAT = 5
PIN_CLK = 6

referenceUnit = 1 # ここの値を決めたい⭐️

def cleanAndExit():
    print("Cleaning...")
    GPIO.cleanup()
    print("Bye")
    sys.exit()


def main():
    hx = HX711(PIN_DAT, PIN_CLK)

    # キャリブレーションの値を設定
    hx.set_reference_unit(referenceUnit)

    hx.reset()
    hx.tare()
    print("測りたいものを載せてください")
    time.sleep(5)

    while True:
        try:
            val = hx.get_weight(5)
            print(val)

            hx.power_down()
            hx.power_up()
            time.sleep(1)
        except(KeyboardInterrupt, SystemExit):
            cleanAndExit()
        except Exception as e:
            print(f"予想外のエラーが発生しました: {e}")
            cleanAndExit()

if __name__ == "__main__":
    main()


結果


この値から、キャリブレーション値を出すようにコードを変更します。

import sys
import RPi.GPIO as GPIO
from hx711py.hx711 import HX711

PIN_DAT = 5
PIN_CLK = 6

REAL_WEIGHT = 48 # ここ追加
SAMPLE_DATA_LIST = [
    17402.111111111124,
    17409.111111111124,
    17414.111111111124,
    17460.111111111124,
    17401.111111111124,
    17403.111111111124,
    17431.111111111124,
    17461.111111111124,
    17442.111111111124,
    17442.111111111124,
    17462.111111111124,
    17443.111111111124
] # ここ追加 先ほどターミナルに出た重さをコピペ⭐️
referenceUnit = sum(SAMPLE_DATA_LIST) / len(SAMPLE_DATA_LIST) // REAL_WEIGHT # ここ追加


def cleanAndExit():
    print("Cleaning...")
    GPIO.cleanup()
    print("Bye")
    sys.exit()


def main():
    hx = HX711(PIN_DAT, PIN_CLK)

    # キャリブレーション値(referenceUnit)を設定
    hx.set_reference_unit(referenceUnit)

    hx.reset()
    hx.tare()
    print("測りたいものを載せてください")
    time.sleep(5)

    while True:
        try:
            val = hx.get_weight(5)
            print(val)

            hx.power_down()
            hx.power_up()
            time.sleep(1)
        except(KeyboardInterrupt, SystemExit):
            cleanAndExit()
        except Exception as e:
            print(f"予想外のエラーが発生しました: {e}")
            cleanAndExit()

if __name__ == "__main__":
    main()


大体48gあたりになっています。これでキャリブレーションの調整が完了です。
キャリブレーション値を出力 print(referenceUnit)すると、363.0 が出力されるので、以下のコードからは、referenceUnit = 363.0を使用します。
 

10分おきに重さを測定する

サンプルとして、猫の食べるご飯を計量します。

10分置きに重さを測るプログラムを追加しました。

import sys
import RPi.GPIO as GPIO
from hx711py.hx711 import HX711

PIN_DAT = 5
PIN_CLK = 6
referenceUnit = 363.0


def cleanAndExit():
    print("Cleaning...")
    GPIO.cleanup()
    print("Bye")
    sys.exit()


def main():
    hx = HX711(PIN_DAT, PIN_CLK)

    # キャリブレーション値を設定
    hx.set_reference_unit(referenceUnit)

    hx.reset()
    hx.tare()
    print("測りたいものを載せてください")
    time.sleep(5)

    while True:
        try:
            hx.power_up()
            # センサが安定するのを待つ
            time.sleep(1)

            val = hx.get_weight(5)
            print(val)
            # 次のデータ取得まで省電力状態にする
            hx.power_down()

            time.sleep(60*10-1) # ここ追加⭐️

        except(KeyboardInterrupt, SystemExit):
            cleanAndExit()
        except Exception as e:
            print(f"予想外のエラーが発生しました: {e}")
            cleanAndExit()


10分おきに、重さが出力されました。
餌は1カップで、およそ68g(全体)-21g(カップ) = 47gでした。
 

データを保存する

重さを測定するファイルがあるディレクトリ内で、保存するデータを入れるフォルダを作成します。
今回は、feeding_recordフォルダを作成しました。

import time
import sys
import RPi.GPIO as GPIO
from hx711py.hx711 import HX711
import datetime
import csv

FOLDER_NAME = "feeding_record"

PIN_DAT = 5
PIN_CLK = 6
referenceUnit = 363.0


def cleanAndExit():
    print("Cleaning...")
    GPIO.cleanup()
    print("Bye")
    sys.exit()


def main():
    hx = HX711(PIN_DAT, PIN_CLK)

    # キャリブレーション値を設定
    hx.set_reference_unit(referenceUnit)

    hx.reset()
    hx.tare()
    print("測りたいものを載せてください")

    while True:
        try:
            hx.power_up()
            # センサが安定するのを待つ
            time.sleep(1)

            current_time = datetime.datetime.now()

            val = hx.get_weight(5)
            print(current_time, val)
            # 次のデータ取得まで省電力状態にする
            hx.power_down()

            # データの登録
            write_to_file(current_time, val) # ここ追加⭐️

            time.sleep(10*60)

        except(KeyboardInterrupt, SystemExit):
            cleanAndExit()
        except Exception as e:
            print(f"予想外のエラーが発生しました: {e}")
            cleanAndExit()

#### ↓の関数を追加 ###
# 上でファイル名を指定する必要がある
def write_to_file(time, feed_weight):
    file_name = time.strftime("%Y-%m-%d")
    formatted_time = time.strftime("%Y-%m-%d %H:%M:%S")
    file_path = FOLDER_NAME + "/" + file_name
    data_list = [formatted_time, feed_weight]

    with open(file_path, "a", newline ="", encoding="utf-8") as file:
        writer = csv.writer(file)
        writer.writerow(data_list)


if __name__ == "__main__":
    main()

これで10分おきに重さを測り、データ保存をするプログラムができました。この後は、実際に猫の餌をはかりに乗せてデータを集めます。


↑ 重量センサーを怪しんでいる様子。
重量センサーとラズパイがそのまま見えると猫が警戒するので、ラズパイはティッシュケースの中に入れました。それでもまだ怪しいようで、疑いの目でこちらを見ています。
大好きな煮干しを皿に置いたら、すんなり受け入れてくれたようでした。ちょろい。

24時間、10分ごとにえさの重さをはかって取得したデータが↓です
ファイル名:feeding_record/2024-7-06.csv

2024-07-06 00:02:14,50.34450323339219
2024-07-06 00:12:14,48.69106407995304
2024-07-06 00:22:14,48.421222810111765
2024-07-06 00:32:15,45.27572016460912
...
中略
...
2024-07-06 23:22:55,78.154027042916
2024-07-06 23:32:56,75.88418577307473
2024-07-06 23:42:56,77.82333921222816
2024-07-06 23:52:56,75.91064079952976

 

データ整形とグラフ化

データが取得できたので、今度はこれをグラフ化する前にデータ整形を行います。
生データには、2つの課題があります。

① 猫の餌の追加が考慮されていない
前に記載した写真の通り、家ではスコップでえさをすくって、2回に分けて1日の中で与えています。1カップ47gなので、30g以上増えればえさが追加されたと判定し、10分間で食べた量=10分前の餌の重さ – 餌の重さ – 47g(追加された餌の重さ)とするプログラムを追加します。

② 猫の衝撃が考慮されていない
猫が餌を食べる際にデータ取得した場合、猫の衝撃が加わったデータになっているため、そのデータは無効値として、グラフ化する情報の中には取り込まないようにします。

そのため、まず猫がえさを食べる時の衝撃はどれくらいなのかを0.1秒ごとに重さを測って計測してみました。猫が好きな煮干しをおいて、猫が計りに当たっている時の数値の動きを計測しました。

2024-07-05 22:51:26,39.68930041152265
2024-07-05 22:51:27,43.88506760728985 ←猫がはかりに当たっている
2024-07-05 22:51:29,39.40094062316286
2024-07-05 22:51:30,39.45385067607292
2024-07-05 22:51:31,40.72369194591418 ←猫がはかりに当たっている
2024-07-05 22:51:33,50.04908877131101 ←猫がはかりに当たっている
2024-07-05 22:51:34,39.893004115226354

はかりの揺らぎはありますが、猫がはかりに当たる時、1.0g~10.0gほど、重くなることがわかりました。

→ どれくらい重さが増えたら猫が当たっていると判定するかの境界値を決め、FEED_THRESHOLD変数に定義しました。猫の重さが加わったと判定したら、猫の食べた量の差分を求める際に、参考としないようにします。

import csv
import matplotlib.pyplot as plt
import japanize_matplotlib

ADDITION_FEED_AMOUNT = 47 # 1度に追加する餌の重さ
FEED_THRESHOLD = -30 # 差分から餌が追加されたと考える閾(負の数)
FILE_PATH = "feeding_record/2024-7-06.csv"

timestamp_list = []
difference_list = []

# ファイルの読み取り
with open(FILE_PATH, 'r') as file:
    reader = csv.reader(file)
    data = list (reader)

i = 0
while i < len(data): 
    timestamp_prev, value_prev = data[i-1]
    timestamp_current, value_current = data[i]

    dt = datetime.datetime.strptime(timestamp_current, "%Y-%m-%d %H:%M:%S")
    hour_current = dt.hour
    minute_current = dt.minute

    if  i == 0:
        # 時刻の登録と餌の重さの差分を追加
        timestamp_list.append(f"{hour_current}:{minute_current}")
        # 1番はじめの記録は1つ前の記録がないため、差分を0とする
        difference_list.append(0)
        i += 1
        continue

    value_prev = float(value_prev)
    value_current = float(value_current)
    difference = value_prev - value_current

    # えさが追加以外でえさの量が減った場合は、計量のゆらぎとしてデータを無効化する
    if FEED_THRESHOLD < difference <= 0:
        # 時刻の登録と餌の重さの差分を追加
        timestamp_list.append(f"{hour_current}:{minute_current}")
        # 正確に計測できなかったデータは差分を0とする
        difference_list.append(0)

        del data[i]
        continue

    # 餌が追加されたと判定し、えさの量を追加する
    if difference <= FEED_THRESHOLD:
        difference += ADDITION_FEED_AMOUNT

    # 時刻の登録と餌の重さの差分を追加
    timestamp_list.append(f"{hour_current}:{minute_current}")
    difference_list.append(difference)
    i += 1

plt.figure(figsize=(10,6))
plt.bar(timestamp_list, difference_list, color="skyblue")

plt.title("10分ごとの猫が食べたえさの量", fontsize=15)
plt.xlabel("時刻",fontsize=15)
plt.ylabel("食べた量(g)",fontsize=12)
# 時刻が重なって見えづらいため角度をつけている
plt.xticks(rotation=45)

plt.tight_layout()
plt.show()

作成されたグラフがこちら

み、見づらい・・・! 注目したところにマークしました↓

🟢 緑の丸→ 餌を追加した時間(11:30ごろ・21:30ごろ)
⭐️ → 8:32と15:32
🔴 赤の丸→ 左の丸が夜0時〜2時の時間帯。たくさん食べている。右の丸が10時半~15時間帯。あまり食べていない。
 

わかったこと

  • 食欲
    餌を追加した直後、たくさん食べるわけではなく、鮮度が良いほど食べるわけでもないことがわかった(🟢緑の丸と⭐️から)
  • 食事のタイミング
    夜の方が昼よりも多く食べていた。猫の食べるタイミングが周期的なら、夜には多めに、昼には少なめに餌を与えるのが効果的かも(🔴赤の丸から)
  • ラズパイの安定性
    一日中ラズパイを稼働させることに熱暴走が起こらないか心配だったが、ファンを追加することで一日中ラズパイを安定して稼働させ、途中で切れることなくデータを取得できた

今回は1日分のデータを取ってみましたが、これだけでは周期性があるのか、ランダムなのかわかりません。鮮度の高いカリカリを食べさせるには、長期的なデータ収集と精度の高い分析が必要です。そのため、今後はAWSのデータ分析サービスを活用してみたいなと思っています。

皆さんも愛猫や愛犬のデータを集めて、グラフ化してみませんか? ペットの健康を見守る新しい楽しみが見つかるかもしれません。
 
 
明日のブログリレーは、ヤマダ(北野)さんの「 初心に帰ってAWS FISで実験を楽しむ」です。周りで楽しんでいる人を見ると自分もやってみたくなりますよね。私もこの記事を読んで、楽しんでFISについて学ぼうと思います。ぜひ明日のもくもく会ブログリレーもご覧ください!