2018/08/23

メダカ自動餌やり器

夏休みに家族旅行に出ることになりました。

旅行中でも餌あげられるように調べてみると、魚自動給餌器って2000円も出せば手に入るんですね?

でも今年は手元にいいオモチャがあるんで、それで魚自動給餌器作ってみました。去年はメダカを実家に預けたのだけど、今年はうちで面倒見ちゃいます!

その前に、どう作るかですよね。。

いくつか考えて見たけど、メカ的機構に凝ると大変なので(というかそういう作り込み苦手なので)簡単な物で済ませました。

原理は簡単♪
香辛料の容器みたいに穴の空いた容器を穴を下にすれば、餌がこぼれ出てくるようなモンです。容器を下にする時間をコントロールすれば、餌の量も調整可能です。


穴を上にすれば止まるし。
そうそう、この筒を回転させれば、それが出来るんですよね。回転速度と回数を変えれば餌の量もコントロールできそうです。一回転とかキッチリ回すにはステッパモータが丁度いいですよね〜。

さて、100均で買ってきた容器に穴を開けて、餌を入れてみて穴から餌が出てくることを確認してみます。3φ程度でいい感じです。

そこまで確認したら早速工作してみます。

材料はこんな感じ

モーターは後述する Solution Kit に付属のステッパモータを使います。この軸が6φ何ですけど、どう接続するか悩んだ挙句、秋葉原のヴィストンロボットセンターに飛び込んで探してみました。ロボット関連のお店なら何かあるだろうと目論んで。そして見つけたのが写真左上に写っているタミヤの AO-8038 ギヤードモーターハブシャフト。これがぴったりハマりました。

イモネジでモータに固定します

付属するネジはナイロン入りで締めるのが大変なので、別途4φのナットを購入です。そして途中まで組み立てたのですが、、水槽固定用に買ってみた部品が入らない!甘かった。これは別途解決しました。

お、おぅ。入らない。。

さて、この機構を早速動かしてみます。

そこで活躍するのが、オン・セミコンダクターモータ・ドライバ・ソリューション・キット。現在、DCブラシモータまたはステッパを回せる LV8548 のキットが MouserDigiKey で入手可能です。

電源が別途必要なんですが(秋月で売ってる電源でOK。LV8548 は 4〜12V が適当)、他はモータ含めてキットに入ってるので、モータと PC をくっつけて、付属の Arduino Micro に Web から落としてきた GUI 用のファームを Arduino IDE を使って書き込んであげて、Windows の GUI を立ち上げると GUI からモータを回せちゃいます。マイナスドライバーも附属してるので、半田付けすらいらないです。

※さりげなく macOS High Sierra 上の VirtualBox から Windows10 走らせて、そこで GUI を立ち上げています。保障外でしょうが、一応動いています。ただし、VirtualBox 上の Windows10 から Arduino IDE を使ってプログラムの書き込みは出来ないので、Mac 上で動かしてる Arduino IDE からファームを書いています

こんな画面でポチポチと

使い方は何となく分かりますね。注意するところはステッパなので、ステップ角(Step Angle) をセットしないと有効にならない設定があるところぐらいですか。ステップ角はモータの仕様を見て、適宜設定してください。

あとは GUI でポチポチとパラメータを変えながら最適な餌の量を落とせる設定を探してみます。回転周波数と餌の量のグラフでもと思ってわざわざディジタルの計りまで買ったのだけど、餌の重さが小さすぎて面倒なのでやめました。さて、いい感じに餌が落ちるようになったら、Generate Program というボタンがあるので、押してみます。

生成されたスケッチ

今までの操作が、API を使った Arduino のスケッチの形で出力されます。

必要なのは初期設定とモータを回す、止めるといった部分のコードだけなので、余計な部分ぽいコードはガシガシ削除。あとは定時に餌をあげられるように時計などのコードを実装したら完成です!

簡単、簡単〜。



こんな風に回ります



実際動かしてみると、ステッパーモータのガタガタした動きが、実はいい感じに瓶をシェイクしてくれます。餌の固まりを防げるし、餌の出が良くなると思います。むしろマイクロステップなんぞいらないですよ。このアプリケーションに関しては。

Full Step と Half Step の違いも試してみましたが、どっちもどっちな感じです。LV8548 は単純な構成なので、Half Step だとステップが細かくなって滑らかになるはずが、トルクが一定でないので味のある動きをします。LV8702 だと Half Step でもトルクが一定になるよう電流のコントロールをするモードがあるのですが(詳しくはデータシート参照)、滑らかになって餌やり器には多分都合が悪いです。

ちなみに LV8548 を搭載した場合はベースボードにある CN8 の A2-A5 端子が全部使えます(Arduino Micro の A2-A5 に直結されてる)。温度センサーをつけて、水温次第で餌をあげる回数や量をコントロールするのも楽しそう!

あと、プチ改造ですが、


ベースボードの VCC と VIN をショート


ベースボードの VCC と Arduino の VIN を直結してあげると、USB から Arduino に給電する必要がなくなるので、モータ用の電源だけでボードを動作させることができます。
Arduino の VIN は 7V〜12V なので、モータに給電する電源次第では Arduino がうまく動かなくなったり、壊したりするので要注意です。

さ、これで安心して旅行に出かけられます!じゃ!

ソースコードはこちら↓
#include <LV8548_STEP_Lib.h>
#include <TimerOne.h>
#include <TimerThree.h>

// LV8548 Stepper library のインスタンス化
Lib_LV8548Step Lib;

/**
 * @brief モーターの制御を表す値
 *
 * hms_def 時間構造体の変数 r に格納される
 */
enum {MT_FEED = 1, MT_FREE = 0};

/**
 * @brief 時間格納構造体
 *
 * モーター制御用の変数を含む。RTC またはシーケンサーに使われる。
 */
typedef struct {
  uint8_t h; ///< 時
  uint8_t m; ///< 分
  uint8_t s; ///< 秒
  uint8_t r; ///< モーター制御
} hms_def;

// RTC 用、時間変数
hms_def rtc_time = {0, 0, 0, MT_FREE};

// シーケンサー用時間変数の配列
// 好きなように設定してして下さい
// ここでは毎回励磁を停止していますが、止めなくても大丈夫です。
// むしろ励磁しておくと止まっている位置から瓶が回転しにくくなるので、
// 不用意に穴の位置が変わってしまうことを低減できます。モーターは熱くなりますが。
hms_def timer_list[] = {
  { 7, 30,  0, MT_FEED}, ///< 朝の餌やり開始
  { 7, 30, 15, MT_FREE}, ///< 餌やり終了、360度回転後少ししてからモーターの励磁を停止
  {12,  0,  0, MT_FEED}, ///< 昼の餌やり開始
  {12,  0, 15, MT_FREE}, ///< 餌やり終了、360度回転後少ししてからモーターの励磁を停止
  {16, 30,  0, MT_FEED}, ///< 夜の餌やり開始
  {16, 30, 15, MT_FREE}, ///< 餌やり終了、360度回転後少ししてからモーターの励磁を停止
};

// シリアル通信用の文字列バッファと Index 用変数
char read_buff[5]; // FMT: x:dd + \0
int buff_ptr = 0;

/**
 * @brief setup()
 */
void setup()
{
  // コマンド入出力用
 Serial.begin(19200);
  
  // LV8548 のための初期化
 Lib.initLib();
 Timer1.initialize(65);
 Timer1.attachInterrupt(interrupt);
  
  // RTC 用の 1秒割り込みを Timer3 で作る
  Timer3.initialize(1000000); // 1sec
  Timer3.attachInterrupt(interrupt3);
 delay(100);
 
  // ステッパーの1ステップの角度設定
 Lib.setStepAngle(7.5);  
}

/**
 * @brief Timer1 割り込みハンドラ
 *
 * LV8548 Stepper library が使用している。
 */
void interrupt()
{
 Lib.timerFire(100);
}

/**
 * @brief 24時間時計
 *
 * リアルタイム更新される時計で、時、分、秒の値を保存する。一秒に一度呼び出されることを期待しています。
 */
void updateRtc()
{
  // Inc 秒
  rtc_time.s += 1;
  if (rtc_time.s == 60) {
    rtc_time.m += 1;
    rtc_time.s = 0;
  }
  // Inc 分
  if (rtc_time.m == 60) {
    rtc_time.h += 1;
    rtc_time.m = 0;
  }
  // Inc 時
  if (rtc_time.h == 24) {
    rtc_time.h = 0;
  }
}

/**
 * @brief hms_def 時間構造体の時間要素の比較
 *
 * 時、分、秒の値を比較する
 *
 * @param  st      ソース時間
 * @param  dt      ディスティネーション時間
 * @retval true    時間一致。
 * @retval false   時間不一致。
 */
 bool timeIsEqualTo(hms_def *st, hms_def *dt)
{
  return ((st->h == dt->h) &&
          (st->m == dt->m) &&
          (st->s == dt->s));
}

/**
 * @brief hms_def 時間を表示する
 *
 * %H:%M:%D のフォーマットでシリアルに出力。
 *
 * @param  t 表示したい時間構造体
 */
void printTime(hms_def *t)
{
  Serial.print(t->h);
  Serial.print(":");
  Serial.print(t->m);
  Serial.print(":");
  Serial.print(t->s);
}

/**
 * @brief hms_def 時間を表示する
 *
 * Timer3 の割り込みハンドラ
 */
void interrupt3()
{
  // RTC の時間を更新する
  updateRtc();

  // RTC の時間をシリアルに出力
  printTime(&rtc_time);

  // シーケンサーの時間を1つずつ確認する
  for (int i = 0; i < sizeof(timer_list) / sizeof(hms_def); i++) {
    // 現在時刻と一致するかどうか
    if (timeIsEqualTo(&rtc_time, (timer_list + i))) {
      if (timer_list[i].r == MT_FEED) {
        // 餌を与える時間
        // モーターの回転速度次第で餌の量が変わりますので
        // 適宜第一引数を調整して下さい。
        // Half step はステップごとにトルクの変動があります。
        // Full step との振動の違いも比較すると面白いです。
        Serial.print(" <- FEED");
        Lib.motorRotationDeg(12, 360.0, 0, 1);
        
      } else if (timer_list[i].r == MT_FREE) {
        // 餌を与えた後、モーターが回り切ってから低消費電力のため励磁を停止する。
        Serial.print(" <- free");
        Lib.motorRotationFree();
        
      }
    }
  }
  Serial.println("");
  
}

/**
 * @brief シリアルで受信した文字列をパースする
 *
 * RTC の時、分、秒を更新することができる。
 * 設定範囲外の場合は無視される。
 *
 * @param  t 受信文字列
 */
void parseBuff(char* s)
{
  int para = atoi(s + 2);
  switch (s[0]) {
    case 'h':
      if (para >= 0 && para < 24) {
        rtc_time.h = para;
        Serial.print("Set h = ");
        Serial.println(para);
      }
      break;
    case 'm':
      if (para >= 0 && para < 60) {
        rtc_time.m = para;
        Serial.print("Set m = ");
        Serial.println(para);
      }
      break;
    case 's':
      if (para >= 0 && para < 60) {
        rtc_time.s = para;
        Serial.print("Set s = ");
        Serial.println(para);
      }
      break;
  }
}

/**
 * @brief シリアルで文字列を受信する
 *
 * 文字列バッファー read_buff のサイズに達した場合、もしくは '\n' (LF) を
 * 受信した場合は文字列の最後に '\0' (NULL) を埋め込み、parseBuff() をコールする。
 *
 * @param  t 受信文字列
 */
void serialRead()
{
  if (Serial.available() > 0) { // 受信したデータが存在する
    char c = Serial.read(); // 受信データを読み込む
    read_buff[buff_ptr++] = c;
    if (buff_ptr == (sizeof(read_buff) - 1) || c == '\n') {
      read_buff[buff_ptr] = '\0';
      buff_ptr = 0;
      parseBuff(read_buff);
    }
  }
}

/**
 * @brief loop()
 */
void loop()
{
  serialRead();
  delay(100);
}