xor

二兎を得るか、一兎をも得ざるか

自作孵卵器でヒヨコを孵化させる【その2・試運転と修正および入卵】

xor.hateblo.jp

試運転結果

温度38.5度設定で1日ほど試運転をしてみて、卵を置く部分に設置している独立した温度計と誤差がないか、湿度は確保されているか、などを確認します。

温度制御はON/OFFではなくやはりPIDに

f:id:ukkz:20200706013402p:plain

温湿度センサ値は30秒に1回の頻度でAmbientにデータをアップロードしています。
データとグラフは公開しているのでこちらから見れます。

f:id:ukkz:20200704012359p:plain

ON/OFF制御だけでは変動幅が大きかったので、結局パラメータ(係数)を小さめにしたPID制御を使うことにしました。
計測値としては、38.5度の設定値に対して0.2度ほどのオフセットとハンチングが認められます。PIDパラメータの調整の余地があります。
ただし、上記写真にある温度計を確認していると、そこまで変動することなく38.5度付近を保っていたので、これぐらいの変動は問題ないかなと思っています。容器や卵自体にもある程度は熱容量がありますし。

水入れの開口部は極小に

f:id:ukkz:20200706013427p:plain

ヒーターの上に水の入ったパウンドケーキの型を置くことにより、温められた水を蒸散させて湿度を確保していました。
しかし半日ほど経って見てみると、湿度90%超で内部に結露ができてしまいました。

www.tori-shime.com

一部孵卵中の良く陥る失敗例で、湿度が高すぎて卵の殻自体が結露し、卵が呼吸できなくなったと言う話もあります。

経験上、孵卵後期に湿度が足りずにヒナが出てこれないというのはよくありましたが、逆に高すぎてもダメだそうです。
というわけで、もう少し湿度を下げられないか試してみました。

f:id:ukkz:20200706013449p:plain

別の細長い容器を買ってきてアルミホイルを3分の2ほどかぶせてみますが、これでもダメ。90%を超えてしまいます。
本来は換気用ファンを設置して、ある程度以上の湿度になったら排気をすると良いのですが、能動的に制御する対象を減らしたいので、もう少しハードウェアを工夫します。

f:id:ukkz:20200706013528p:plainf:id:ukkz:20200706013658p:plain

今度は、細長いフタ付きのタッパーを買ってきました。幅はピッタリ、耐熱温度も140度あります。
(頑張って探すと何かしらあるから100均工作はやめられんな!ガハハ)
これのフタに小さい穴を開けることで、水蒸気の量を調節します。

f:id:ukkz:20200706013720p:plain

さらに、気密性がこのままでもまだ高いようなので、外装容器のエッジ近くに点々とφ4ほどの穴をあけて、内部の温まった空気がある程度抜けるような構造にします。
これを行ったあとの温湿度状態が一番最初の写真のとおりです。わりと面倒でした。

f:id:ukkz:20200706014011p:plain

あとになってから、この水入れ容器に給水するときのことを考えて漏斗をフタにくっつけてみました。
加温中の容器内部はかなり結露していますが、漏斗をつけても意外と蒸散量はあまり変わらないようです。

f:id:ukkz:20200706014423p:plain

さて、ここまでやりきったらあとは卵を入れて21日間放置しておけばOKです。
慣習的に卵にはそれぞれ番号を書いておき、検卵時に記録できるようにします。が、今回は入卵後にしばらく別の地域で仕事をするため、次の報告は産まれたかどうか? だけになるかと思います。
それを見越して、18.5日後 - 加温開始から444時間後に転卵が自動で止まるような仕組みもプログラムに入れておきました。

f:id:ukkz:20200706014543p:plain

地鶏の卵6個 + 烏骨鶏卵3個の計9個をケースごと転卵カゴにセットします。
烏骨鶏卵は別の卵専用ケースの一部を切り取ったものに入れ、しかも地鶏のよりも一回り小さいサイズで転卵時に落ちそうなので、ティッシュでスペースを埋めるようにして設置しました。

さて、何羽ほど産まれるのでしょうか。3週間後が楽しみです〜


技術的なこと(おもにソフトウェアまわり)

以下は技術的な補足というかメモに近い云々になります。

ソースコードはこちら

github.com

SPIFFSを使って設定値を分離する

WiFiパスワードとかMQTT情報とかがGit上に乗っかってしまうの回避と、あとからシリアルかMQTTで動的に設定値変更したくて実装しました。
Config.h とREADMEを参照してください。
Arduino系のコードってなかなかgitignoreなんてやりにくそうなので、同じようなのを作ってる人居そうな気がします(調べてない)。

ESP32のFreeRTOSでタスクを割ってタイマー制御する

孵卵器においては以下の動作を定期的に実行する必要があります。

  • 温度取得とヒーターの制御(1秒毎)
  • 画面表示の更新(1秒毎)
  • 転卵(およそ1時間毎)

Arduino IDEでの開発ですが、これらのタスクをメインの loop() 内とかで実現しようとすると冗長になってしまいカッコ悪いです。
せっかくのデュアルコアRTOSが使えるESP32なので、ここはタスクを作成してタイマーによって駆動させました。

lang-ship.com

lang-ship.com

FreeRTOSをがっつりやり始めると気が狂いそうになるので、上記のようなページから知見を頂いて付け焼き刃的に実装しました。
関数リファレンスとしては、日本語だと以下が一番詳しい気がしました。

lang-ship.com

今回のタスクとタイマー周りはざっくり以下のような感じになっています。

  • 毎秒実行タスク(ヒーター制御と画面更新・30秒毎にAmbientアップロード)
  • 転卵タスク(サーボうごかすだけ)
  • 上記2つを setup() の最後で稼働させて loop() ではなにもしない
  • 444時間(18日半)経過したら転卵タスクを止める

毎秒実行のほうは適当でもいいとして、少なくとも卵にやさしく転卵するためにサーボモータを細かく delay を与えながら制御する必要があります。
それを踏まえると、タイマーは使わなかったとしてもFreeRTOSのタスクはやはり非常に便利でございました。

// タスクを登録する
xTaskCreatePinnedToCore(rotateTask, "RotaterRun", 2048, NULL, 2, &rotateTaskHandle, PRO_CPU_NUM);
// 一時停止する
vTaskSuspend(rotateTaskHandle);

// 以下を関数にしておいて切り替えたい任意の場所で呼び出す
if (onoff) {
  // 転卵タイマーを設定・rotateTimerはグローバル宣言しておく
  rotateTimer = timerBegin(TIMER_NUM_ROTATER, 80, true); // TIMER_NUM_ROTATERはタイマー番号
  timerAttachInterrupt(rotateTimer, &rotate, true); // rotateはグローバル定義した割り込み関数
  timerAlarmWrite(rotateTimer, 60 * 1000000UL, true); // 60秒*100万マイクロ秒・繰り返し有効
  // 転卵タスクを再開状態にする(切り替えたらすぐに動き始めるように見える)
  vTaskResume(rotateTaskHandle);
  // 転卵タイマー開始
  timerAlarmEnable(rotateTimer);
} else {
  // 転卵タイマー停止
  timerEnd(rotateTimer);
  rotateTimer = NULL; // 繰り返し有効の場合NULLにしておかないと勝手にタスク登録されるので注意
  // サーボの位相を中立位置に戻して停止するタスクを登録・即実行(実行後タスク内のvTaskDeleteによりワンショットで終了)
  xTaskCreatePinnedToCore(rotateStop, "RotaterStop", 2048, NULL, 2, NULL, PRO_CPU_NUM);
}

転卵のタスクの実行と停止に関しては、 setup() 内でタスクを登録しておいてすぐにサスペンドさせて止めておき、オンオフの設定値に応じてタイマーへの登録と解除を行います。
登録しただけでは実行されないと思ってましたが、 xTaskCreatePinnedToCore の直後に eTaskGetState で状態取得するとReadyStateではなく eRunning になっているようす。
タスクハンドラをNULLにして登録したら、先行して動作しているタスクが無い限り即時実行開始となるのかもしれません(くわしい人教えてください)。

MQTTで遠隔制御する

温度調整にPID制御を使っていますが、安定していないときにPID係数を調整するためにスケッチ書き込みをするのは大変です。
そこでMQTT経由で遠隔からコマンドを飛ばし、各種設定値を変更できるようにしました。
サーバー側であるMQTTブローカは自作のものを利用しています。

mqtt.uko.jp

誰でも利用可能ですが、24時間毎にパスワードが自動で変更されてしまいます。
この記事を読んで似たようなのを作りたいなと思った方は、アカウント作りますのでコメントなどでご連絡ください。(ただし無保証です)

例として、転卵角度を45度にしたいときは以下のように mosquitto のコマンドを打てば変更できます。もちろんレスポンスもあります。

// Publish(コマンド送信)
$ mosquitto_pub -h mqtt.uko.jp -p 1883 -u USERNAME -P PASSWD -t /topic/for/send -m "set degrees:45"

// Subscribe(レスポンス受信)
$ mosquitto_sub -h mqtt.uko.jp -p 1883 -u USERNAME -P PASSWD -t /topic/for/receive
~ [2020/07/06(Mon) 06:00:00] [SET] degrees to 45

追記:
mosquittoのpubとsubをシェルスクリプトにまとめて通常のCUIコマンドのように扱えるようにしました。
詳細はGitHubリポジトリ内の README > "Control via MQTT" の項を参照してください。

クライアント側は以下のPubSubClientを利用します。

PubSubClientをタスク内で上手に使う

ESP32でMQTT使うとすればPubSubClientほぼ一択ですが、ブローカに繋ぎにいくときの関数がブロッキングのため、タスク上でうまく使わないとウォッチドッグタイマが発動してしまいます。

github.com

このあたりを参考にしつつ、MQTT接続用タスクを作り、ループ関数(client.loop())は通常のloopに入れて動かしてみます。

TaskHandle_t Task1; // core 0 task handler
void setup() {
xTaskCreatePinnedToCore(MQTT_TASK, "Task1", 10000, NULL, 1, &Task1, 0);
}

create function for task
// MQTT task running on Core 0
void MQTT_TASK(void * pvParameters) {
for (;;) {
if (FL_WiFi_Network_Connected && !client.connected()) {
MQTTconnect();
}
vTaskDelay(1000);
//delay(1000); // wait 10 seconds until we re try
}
}

Works like a dream :)

なるほど。
もともとESP32はArduino-IDF開発時にネットワーク周りはCore0でうまいことさばいてますので、MQTTもこっちに振るのが賢い選択ですね。

と思ってたら、AmbientにデータをアップロードするタスクとMQTTのタスクで、 WiFiClient クラスのリソースを奪い合ってしまいエラーが出てしまいました。
(fdってファイルディスクリプタかな…… そんなんあるんだという感想)

[E][WiFiClient.cpp:460] available(): fail on fd 59, errno: 11, "No more processes"
[I][WiFiClient.cpp:512] connected(): Unexpected: RES: -1, ERR: 9
[E][WiFiClient.cpp:460] available(): fail on fd -1, errno: 9, "Bad file number"

対策として、30秒毎にタイマーでレジュームされる1つの通信系タスクを作り、その中にAmbientとMQTTを入れて同期的に動かしてみます。
そうすれば、Ambientはなんとか動きはするようですが、MQTTが毎ループごとに接続が切れたり不安定になったりと、挙動が怪しいです。

// タスク内の1回のループで処理順を変えてみる

// MQTT接続確認 → Ambientアップロード のとき
log_i("%d", mqttClient.state()); // -3 : the network connection was broken

// Ambientアップロード → MQTT接続確認 のとき
log_i("%d", mqttClient.state()); // 0 : the client is connected
log_i("%d", mqttClient.connected()); // 0 : the client is not connected

とこんな感じなので、もしやと思いAmbient.cppを確認すると this->client->stop() との記述を発見しました。こりゃたまらんち。
単純に WiFiClient を2つぶん用意したら問題なく動いてくれました。

f:id:ukkz:20200705072441p:plainf:id:ukkz:20200705072457p:plain

コマンドを解釈して実行できるクラスを作成し、MQTTで何かしら受信したら実行されるコールバック関数内に入れておきます。
上記写真では右側でSubscribe、左側でコマンドをPublishしているところで、自動転卵装置をオンオフしたり任意の角度にしたりしています。
こんな感じでオンラインでの操作と確認が行えるようにしました。

f:id:ukkz:20200705072542p:plainf:id:ukkz:20200705072600p:plain

目標温度の指定、PIDパラメータ調整、孵卵開始日時設定など、一度スケッチとSPIFFS上の設定ファイルを書き込めばあとはほぼワイヤレスで操作ができちゃいます。
需要があったら商品化してみたいものですね。
引き続きコロナ影響は続くので、自宅で孵化させたトリを愛でる人が増えればいいなあなんて思ったり思わなかったり……


つづき↓
xor.hateblo.jp