Tanuki_Bayashin’s diary

電子工作を趣味としています。最近はラズベリーパイPicoというマイコンを使って楽しんでいます

【金魚用】自動エサやり装置の製作

※なにか気になる点がありましたらコメント欄にご記入ください。また、工作や回路を製作する場合には、細かい作業などに対して、細心の注意を払われるようお願いいたします。

【目次】

1.はじめに

金魚を飼っているので、旅行などで不在のときに金魚に自動でエサを与える装置があると便利かと思い、そのような装置を作ってみました。

ホームセンターのペットコーナーでは写真1のような製品が並んでいました。これらを参考に取り組んでみました。

図1 自動給餌装置(Amazonにて)

※なんとか動作するようにはなりましたが、誤作動しないという保証が得られず、あくまで趣味の範囲での一つの試みです。
 この記事を参考にされる場合は、十分に安全に配慮し、自己責任にて取り組まれるようにお願いします。

写真1 完成した装置

2.全体の構成

全体の構成としては、

  • メカ的な装置を作り、それをモーターで動かすことで金魚にエサを与える
  • 1日に2回(朝と夕)、自動でエサを与える
  • 時間の測定にはNTPサーバーを利用する。そのためにWifiにてインターネットに接続する
  • エサやりの後は次のエサを与える時刻までスリープモードに入る

●2つの思案
作るにあたり2つほど方式を思いつきました。

  • アルキメディアン・スクリューを使う(スクリュー方式)
  • 小さな穴をいくつか開け、振動させることで下に落とす(フリカケ方式)

の2つです。

2.1スクリュー方式による装置

アルキメディアン・スクリューとは図2に示すようなものです。古代ギリシャで活躍したアルキメデスが考案したとされるスクリューです。現在でも鉱山などで使われているそうです。

図2 アルキメディアンスクリュー(wikiより)

Silberwolf (size changed by: Jahobr) - File:Archimedes-screw_one-screw-threads_with-ball_3D-view_animated.gif created by Silberwolf, CC 表示-継承 2.5, https://commons.wikimedia.org/w/index.php?curid=2489813による

実際に製作したものを写真2に示します。

写真2 スクリュー方式の自動エサやり装置

黄色い部分が3Dプリンターで作った部分です。モデル等は付録に示します。

動力部分はタミヤの工作シリーズを使いました。使っていないものを引っ張り出してきました。

回路の部分は3章で詳しく見ます。小型のマイコンであるXiao Esp32 C3 というものを使いました。(たまたま昔買っておいたものです)

2.1.1 動作のようす

動作しているときの様子を示します。

youtu.be
上の動画についてですが、エサを入れていないときの様子です。このときのコードではswは有効に働いています。
底面にマジックテープを貼っています。台座に取り付けるためのものです。


youtu.be
エサがあるときの様子です。こんな感じです。

2.2フリカケ方式による装置

こちらは写真3のようになりました。

写真3 フリカケ方式による自動エサやり装置

仕組みとしてはモーターを回すことにより、シャフトにつけたホイールの軸(タミヤ工作シリーズ)を回転させ、その影響で容器が振動します。

振動が起きることで、容器の側面に開けた穴からエサが落ちる、という訳です。

動力に当たる部分は、スクリュー方式とは異なるものですが、同様のものです。(タミヤの工作シリーズ)

また回路の部分はスクリュー方式と全く同じです。

2.2.1 動作のようす

こちらも動作しているときの様子を示します。

youtu.be
上の動画についてですが、エサなしの場合です。これもswが有効に働いています。
エサは入っていないです。こちらも底面にマジックテープを貼ってあります。


youtu.be
エサが入っているときの場合です。振動を受けてエサが落ちています。

2.3 おまけ要素

おまけというには何なんですが、水そうに取り付けるための台も自作しました。
3Dプリンターなどで格好いい物ができるといいのですが、まだそこまでの技がないので、タミヤの工作シリーズの余りなどを利用して作りました。
ちょっとしたものでしょ(エヘン)(;´∀`)

youtu.be
動画にするほどでもないかと思いますが、伝わりやすいかと思い載せてみました。

3.回路について

図3 回路図


・XiaoESP32C3の基本データです
qiita.com

入力電圧は5Vとありますが、4.4Vのリチウムイオンバッテリーである18650を使用しました。

モーターの駆動にはD10(11番ピン)を使用し、トランジスター2SC4811を用いてスイッチの代わりとして使っています。
ベース抵抗は10KΩとし、これによりコレクタ電流は最大で2.7Aまで駆動可能です。(計算上)
(モニター用にLEDもつなぎました)

D8は上の記事を参考にしてプルアップしてあります。

D9にはタクトswを付けましたが、現在ソースコード中では特に使用していないです。
(ディープスリープ中にGPIO割り込みがかけられるようにしたかったのですが、うまく働かなかったのでやめました)

それとWifiを使用したのでアンテナも取り付けました。

4.ソースコード

このシステムを動かすにはxiaoにソースコードを書き込む必要があります。xiao の開発にはArduino IDEを用いました。
リスト1にその内容を示します。

リスト1

#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <esp_sleep.h>
#include <esp_bt_main.h>
#include <esp_bt.h>
#include <esp_wifi.h>

//RTC coprocessor領域に変数を宣言することでスリープ復帰後も値が保持できる
RTC_DATA_ATTR int COUNT = 1;  // 定刻の配列のインデックス
RTC_DATA_ATTR bool firstTime = true; // このコードの初回起動時ならtrue

const char* ssid      = "";
const char* password  = "";

const int ledPin = D10; // LEDが接続されているピン番号

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000);

unsigned long epochTime = 0;      // エポックタイムを保存する変数
unsigned long referenceTime = 0;  // 時刻を秒で表現する変数
float hours = 0.0;                // 時刻を計算
unsigned long waitTime = 0;       // ディープスリープの時間を保存する変数

#define TIME_MAX 2    // 起床時間(wakeupTime)の要素の数
float wakeupTime[ TIME_MAX ] = {8.0, 19.0}; // 起床時間

void setup() {
    Serial.begin(115200);
    pinMode(ledPin, OUTPUT);

    Serial.printf("Cause:%d\r\n", esp_sleep_get_wakeup_cause());
    wakeup_cause_print();

    // WiFi接続
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        Serial.println("Connecting to WiFi...");
        delay(1000);
    }
    Serial.println("Connected to WiFi");

    // NTPクライアントの初期化と時刻の更新
    timeClient.begin();
    timeClient.update();

    // エポックタイムを取得して保存
    epochTime = timeClient.getEpochTime();

    // 1日ごとの秒数 3600*24=86400
    // UTG との時差+9時間
    referenceTime = (epochTime + 3600 * 9) % (3600 * 24);
    // 時刻を計算
    hours = (float)referenceTime / 3600.0;

    Serial.println("referenceTime, hours:");
    Serial.println(referenceTime);
    Serial.println(hours);

    // 給餌は8時と19時
    // 定刻に起床したかどうかの判定
    // (時間を分に換算している)
    float diff = (wakeupTime[COUNT] - hours) * 60.0;
    
    // 定刻10分以内に起床したときにLEDをon(diffがマイナスの場合も考慮している)  
    if((diff * diff) < 100.0){
      // 8時と18時にLEDをオン
      digitalWrite(ledPin, HIGH);
      delay(5000); // 10秒間LEDをオン
      digitalWrite(ledPin, LOW);
      referenceTime += 5;

      // COUNTの更新
      COUNT++;
      if(COUNT >= TIME_MAX){
        COUNT = 0;
      }
    }
    // 再度計算 単位は分
    diff = (wakeupTime[COUNT] - hours) * 60.0;
    //waitTimeを計算する
    if(COUNT == 0 && diff < 0){  // 日付が変わる手前で、再度スリープするときの処理
      waitTime = ((unsigned long)wakeupTime[0] + 24) * 3600UL - referenceTime;

    }else{  //それ以外ときの処理。約2分手前で起床するように120を引いている
      waitTime = (unsigned long)wakeupTime[COUNT] * 3600UL - referenceTime - 120;

    }

    //waitTimeを表示する
    Serial.println("waitTime:");
    Serial.println(waitTime);
    delay(50);
    // waitTime--;

    Serial.println("COUNT:");
    Serial.println(COUNT);

    // スリープ前にwifiとBTを明示的に止めている
    // 止めないとエラーになる
    esp_bluedroid_disable();
    esp_bt_controller_disable();
    esp_wifi_stop();

    // ディープスリープモードに入る
    esp_sleep_enable_timer_wakeup(waitTime * 1000000ULL); // waitTime秒後に起床
    esp_deep_sleep_start();
}

void loop() {
    // ループは使用しません
}

// 起床時の要因をメッセージとして表示する
void wakeup_cause_print() {
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch (wakeup_reason) {
    case 0: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_UNDEFINED"); break;
    case 1: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_ALL"); break;
    case 2: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_EXT0"); break;
    case 3: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_EXT1"); break;
    case 4: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_TIMER"); break;
    case 5: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_TOUCHPAD"); break;
    case 6: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_ULP"); break;
    case 7: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_GPIO"); break;
    case 8: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_UART"); break;
    case 9: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_WIFI"); break;
    case 10: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_COCPU"); break;
    case 11: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG"); break;
    case 12: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_BT"); break;
    default: Serial.println("Wakeup was not caused by deep sleep"); break;
  }
}

図4 ソースコードの構成図


全体の流れを図4に示します。リスト中にコメント文もありますし、順を追って読めば解読可能かと思います。


〇稼働時間について
1回のエサやりの時間は5秒です。一定時間にエサが出ていく量は、容器内のエサの量、ボードの傾斜角などでわりと大きく変わります。
そしてエサやりの時間でも吐出するエサの量は変わります。

また金魚のエサの量はおおよそ金魚の体重の0.4%が好ましいとされています。(以下参照)

金魚に与える餌の適量を確認する方法
kingyobu.wordpress.com

筆者の飼っている金魚の場合、4匹合計で約200gなので0.004をかけると0.8gに相当します。
これは5秒間駆動することでエサが落ちてくる値となっています。
(私事でした)


【バグ解決にいたったエラーメッセージ】
リスト中、loop()関数の下にある関数は以下のリンク先の記事を参照させていただきました。

qiita.com

③での関数を利用することにより、バグを一つ解決することができました。

バグの解決のために、起床したときのシリアルモニタでのメッセージを見ると、正常に起床したことをうかがわせる内容でした。
それまでどうしても起床の時間が最大で5分ばかりずれてしまい理由が分からなかったのですが、
正常に起床していると分かり調べ直しところ、Arduinoなど世の中のマイコンのクロックはだいたい1%ほどの精度であるとの記述がありました。

(5 × 60)÷ (24 × 3600) = 0.35[%]
となるので、24時間ほどでで5分ほどの誤差は精度範囲内というところに落ち着きました。

※ただし次の記事では誤差は200ppm程度とのことでした。上で求めた0.35%は3500ppmに当たるので疑問符が残りますが、運用上問題ないので今後の課題とします。
lab.seeed.co.jp


【小ネタ】
Xiaoに出来上がったスケッチを書きこむときには、構成図での電池からの給電を切る必要がありました。そうしないとPCがXiaoを認識しないで、UARTのCOMポートが開かないという症状が発生しました。

5.動画

実際にエサを与えているシーンを貼っておきます。
youtu.be


動画を見て分かるかと思いますが、フリカケ方式は現在使用していないです。
理由としてはエサの量のコントロールがいまいちむずかしいのと、スクリュー方式のみでも運用できているからです。

フリカケ方式には悪いですが、今はエサを入れていない状況で、定刻に1~2分ズレて同じように動いています。
(誤差の影響みたいです)

6.まとめ

自動で金魚にエサを与える装置を作ってみました。
現在も朝、夕に稼働していて、3日に1度程度重さを量って減ったエサの量を計算で求めています。

今後トラブルが起きないか、試験を続けていきます。

付録A スクリュー方式で使用した治具STLファイルと回路図

github.com

上のリンクから3Dプリンターのモデルと設計した回路の回路図(PDFファイル)がダウンロードできます。
ご自由にお使いください。
STLファイルには㏄ライセンスがあります。回路図にはありません)



最後までお付き合い下さり、ありがとうございました