金魚の自動餌やり機を作ってみた

# Motivation
長女が秋祭りの金魚すくいで金魚を持って帰ってきて、エアポンプ付きの小さな水槽買って環境整えて、長女も毎日餌やったり水槽の掃除したり世話してるともう2ヶ月以上元気に泳いでる。旅行等で家を空ける時に餌やりどうしようかなと思って自動餌やり器とか物色してみたが、このくらいなら自作できるんじゃね、ということでググったら、みんなLEDチカ並み?に自作してたので自作してみた。


# Prerequisite
新しく買ったものはありません。
- ESP-WROOM-32D(Arduino互換マイコン): [センサデータをArduinoで取得して、ゲートウェイ経由でsyslogサーバに集めてみた](https://qiita.com/kamotsuru/items/96daba5e217b31d27699)で買ったものが遊んでたので、再利用
- Mini Servo: 昔買った[Arduino Sidekick Basic Kit](https://wiki.seeedstudio.com/Sidekick_Basic_Kit_for_Arduino_V2/)に入ってた。使ったことない。。。
- サーボを固定する治具とか餌を貯めておく容器: これも昔買った3Dプリンタ[Micro 3D](https://micro.printm3d.com)で作った(後述)。

# Mini Servoをタイマ制御する
[はじめての電子工作　Arduinoで金魚の自動餌やり機を自作してみた](https://inomacreate.com/arduino-kingyo/)を参考にMini Servoをタイマ制御しようとしたが、手持ちの純正Arduino Nano 33 BLE Senseではなく、あとで配線を戻しやすい上記ESP-WROOM-32Dを使おうとしたところ、まあ動かない。
ググったところ、CPUアーキテクチャ?が違うらしく、下記を参照してコード修正した。
- [Using Servo Motors with ESP32](https://dronebotworkshop.com/esp32-servo/)
- [ESP32: Ticker library](https://techtutorialsx.com/2021/08/07/esp32-ticker-library/)

下記コードは12時間毎に、初期位置から180度回転したのち、-180度戻るだけです。
ESP-WROOM-32DにはWifiついてるので、[センサデータをArduinoで取得して、ゲートウェイ経由でsyslogサーバに集めてみた](https://qiita.com/kamotsuru/items/96daba5e217b31d27699)同様、餌やりしたときにsyslogサーバにsyslog飛ばすようにしてます。ログだけは旅行先からでも見れる。。。後述するAlexaをしゃべらせるためのCGIアクセスのhttp client実装は[https://101010.fun/iot/http-client-get-post.html](https://101010.fun/iot/http-client-get-post.html)を参考にしました。
```
#include <ESP32Servo.h>
//#include <MsTimer2.h>
#include <Ticker.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Syslog.h>
#include <HTTPClient.h>

#define SYSLOG_PORT 514

Servo myservo;  // create servo object to control a servo
int pos = 0; // initial servo position (degree)

//wifi configuration
char* ssid     = "Wifi Network";
char* password = "guest";
IPAddress wifiIp;
WiFiUDP udpClient;

char* server = "192.168.0.11"; // syslog server
Syslog syslog(udpClient, server, SYSLOG_PORT, "esp32", LOG_KERN);

// timer variables
Ticker periodicTicker;
volatile boolean tsw = true;
int hour_cnt = 0;
int min_cnt = 0;
int sec_cnt = 0;

void setup() {
  // put your setup code here, to run once:
  myservo.attach(2);  // attaches the servo on pin 2 to the servo object
  Serial.begin(115200);
  myservo.write(pos); // tell servo to go to position in variable 'pos'

  periodicTicker.attach_ms(1000, timecnt);  //timecnt() is called every 1000ms

  // We start by connecting to a WiFi network
  delay(10);
  if(Serial) {
    Serial.println(ssid);
  }
  WiFi.begin(ssid, password);
  while(WiFi.status() != WL_CONNECTED) {
      delay(500);
      if(Serial)
        Serial.print(".");
  }
  wifiIp = WiFi.localIP();
  if(Serial) {
    Serial.println("");
    Serial.println(WiFi.localIP());
  }
  syslog.log(LOG_INFO, "setup done!");
}

void loop() {
  // put your main code here, to run repeatedly:
  // timer action
  if( tsw ){
    motor_action();
    syslog.log(LOG_INFO, "motor_action()!");
    tsw=false;

    //call alexa api to speak when motor_action()
    WiFiClient client;
    HTTPClient http;

    if( !http.begin(client, "http://192.168.0.11/speak.sh") ) {
      if(Serial)
        Serial.println("Failed HTTPClient begin!");
    }else{
      if(Serial) {
        Serial.println("HTTPClient begin!");
      }
      http.addHeader("Content-Type", "text/html");
      int responseCode = http.GET();
      if(Serial) {
        Serial.println(responseCode);
        Serial.println(http.getString());
      }
      http.end();
      syslog.logf(LOG_INFO, "responseCode : %d", responseCode);
    }
  }
}

void motor_action() {
  for (pos = 0; pos <=180; pos += 1) { // goes from 0 degrees to 180 degrees
    // in steps of 1 degree
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(3);                        // waits 3ms for the servo to reach the position
    if(Serial) {
      if(pos%10 == 0) {
        Serial.print(pos);
        Serial.print(", ");
      }
    }
  }
  if(Serial)
    Serial.println("");
  for (pos = 180; pos >= 0; pos -= 1) { // goes from 0 degrees to 180 degrees
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(3);                        // waits 3ms for the servo to reach the position
    if(Serial) {
      if(pos%10 == 0) {
        Serial.print(pos);
        Serial.print(", ");
      }
    }
  }
  if(Serial)
    Serial.println("");
}

void timecnt() {
  sec_cnt++;
  if( sec_cnt > 59 )
  {
//    tsw=true;
    min_cnt++;
    sec_cnt=0;
    if( min_cnt > 59 )
    {
//      tsw=true;
      hour_cnt++;
      min_cnt=0;
      if( hour_cnt > 11 )
      {
        tsw=true;
          hour_cnt=0;
       }
    }
    if( min_cnt%10 == 0 || (hour_cnt > 10 && min_cnt > 40))
      syslog.logf(LOG_INFO, "%d h %d min %d sec", hour_cnt, min_cnt, sec_cnt);    
  }
  if(Serial) {
    Serial.print(hour_cnt);
    Serial.print("h");
    Serial.print(min_cnt);
    Serial.print("min");
    Serial.print(sec_cnt);
    Serial.println("sec");
  }
}
```
# サーボを固定する治具とか餌を貯めておく容器を作る
[Autodesk Tinkercad](https://www.tinkercad.com/dashboard)で下記の通り設計して、前述の通りMicro 3D使ってプリントアウト
- [水槽に固定する治具](https://www.tinkercad.com/things/fq0TqQw8xYu)
![Screen Shot 2022-11-26 at 20.57.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117100/cff9fd3d-b55f-f098-656f-a731fe33b5e1.png)
- [餌を貯めておく容器(蓋)](https://www.tinkercad.com/things/hw7LAjP3YpY)
![Screen Shot 2022-11-26 at 20.58.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117100/4606f215-099a-bb5f-decf-2fdbe680c569.png)
- [餌を貯めておく容器(本体)](https://www.tinkercad.com/things/apSD6OqoinY)
![Screen Shot 2022-11-26 at 20.57.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117100/11f78d39-bc59-df6b-c28c-f10aec1819ff.png)
餌を貯めておく容器の横に、回転させた時に餌が出てくるよう穴を5つ開けた。3Dプリントしたものとサーボを組み合わせるとこんな感じに
![IMG_1529.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117100/5802d41a-923e-87ab-65bc-348c46e0ac6e.jpeg)


# 試験
![ezgif.com-video-to-gif-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117100/d7838ffb-d94e-834e-8c13-757363f078b6.gif)
一応動いた。わかりにくいのですが、奥側の開けた穴が垂直下向きになった時に餌がちゃんと落ちます。

# Alexaに通知させる
60秒で回転するように仮設定して試験はしたのですが、所望の時間(12時間毎)で長期安定試験する時つい確認し忘れるので、下記を参考にsyslogサーバとして利用しているraspberry piを経由して、回転したらAlexaにしゃべらせるようにして、Alexaがしゃべったら急いで水槽を見に行き金魚が食べるより先に餌が適量出ていることを確認しました。。。
- [Amazon Echo 自由に喋らせる](https://qiita.com/ABK28/items/a599117acbdc35bbf2a0)
- [alexa-remote-control](https://github.com/thorsten-gehrig/alexa-remote-control)
 
前者サイト「**CGI（シェル）の作成**」の`speak.sh`は引数を取らせず、下記の通りしゃべるセリフは埋め殺しです。
```
echo `/var/www/html/alexa.sh -e "speak:goldfish was fed"`
```
前者サイトにはいくつかtypo?があるようです。
- 「**パッケージでいろいろインストール**」の`apt install fcgiwarp`は`apt install fcgiwrap`です。
- 「**fcgiwrapの設定**」の下記`fastcgi_param`は最後に`/`が要りました。

```
    location ~ \.sh$ {
        try_files $uri =404;
        root   /var/www/html/;
        fastcgi_split_path_info ^(.+\.sh)(/.+)$;
        fastcgi_pass      unix:/var/run/fcgiwrap.socket;
        include           /etc/nginx/fastcgi_params;
        fastcgi_param     DOCUMENT_ROOT /var/www/html/; #最後に/が要る!
        fastcgi_param     SCRIPT_FILENAME   $document_root$fastcgi_script_name;
        fastcgi_intercept_errors    on;
    }
```
後者サイトの[Initial login page has changed #166](https://github.com/thorsten-gehrig/alexa-remote-control/issues/166)にあるように、Alexa API?のURLが変わっており、2023/5時点では、シェルスクリプト内の`https://alexa.${AMAZON}` を`https://alexa.${AMAZON}/spa/index.html`に修正する必要がありました。
また、前者サイト「**alexa-remote-control.shの導入**」の`SET_MFA_SECRET: Amazonの認証コード`は「**認証コード**」の手順に加え念のため、後者サイト「**Old option MFA**」の通り、「新しいアプリの追加」の「バーコードをスキャンできませんか？」で表示されるMFA shared secretを引数として、下記で出力されるワンタイムパスワードをAmazonの登録webフォームに入力してアプリ追加しておきました。
```
oathtool -b --totp <MFA shared secret>
```
私のraspberry piにはoathtoolがインストールされてなかったので、インストールして実行しました。

(2024/5/4追記)
[Login procedure does not work anymore (even with new uri) #171](https://github.com/thorsten-gehrig/alexa-remote-control/issues/171)の通り、Amazon側の仕様変更で上記のユーザ/パスワード/MFAによる認証は昨年末くらいには使えなくなり、トークン認証のみとなりました。
alexa-remote-control [README](https://github.com/thorsten-gehrig/alexa-remote-control)の**Login via REFRESH_TOKEN**の記載の通り、MacOS上に[alexa-cookie-cli](https://github.com/adn77/alexa-cookie-cli)をcloneし、そのディレクトリにコピーした[alexa-cookie-cli-macos-x64](https://github.com/adn77/alexa-cookie-cli/releases/download/v5.0.1/alexa-cookie-cli-macos-x64)をTerminal上で`./alexa-cookie-cli-macos-x64 -p amazon.co.jp`と実行し、同MacOSのSafariからhttp://127.0.0.1:8080/ にアクセス、自分のamazon.co.jpアカウントでログインするとTerminal上に`Atnr|`で始まるrefreshTokenが返ってきます。
このトークンを、raspberry pi上のの[alexa_remote_control.sh](https://github.com/thorsten-gehrig/alexa-remote-control/blob/master/alexa_remote_control.sh)の`SET_REFRESH_TOKEN=''`に設定するとユーザ/パスワード/MFA認証と同様に動いてます。

# 最後に
~~家空ける時にセッティングしますが、ちゃんと動くはず。。。~~