自宅には1階に大きめのメダカの水槽と、2階に小さめのメダカの水槽(2匹とヌマエビ、大小結構いるのであまり数えたことがない。。。)があります。
今回は自宅2階にあるメダカの水槽の照明を「WS2812B」を使ってリモートコントロールしたことを書きます。
今まで水作のLED照明を使ってガンガンに明るくして使っていたのですが、朝・夜と照明を自分でON/OFFしないといけない点、照明を当てすぎているせいか(または、寒い季節なのに餌をやりすぎているのか)、水槽、ろ過装置、少しだけ生えている水草に著しい「コケ」がびっしり生えるようになってしまいました。毎日毎日コケ掃除をしてなんとか状態を保っていたのですが、日に日に水槽のコケの状態がひどくなってきました。
加えて、そのLED照明が明るいせいか、毎回ONにした瞬間にメダカがびっくりして、あせあせと急に動き回るんですよねこれによって、結構ストレスも溜まるんだろうなあとも感じました。
この状態が続くのもメダカにもあまり良くないと思いますので、これを機に一度水槽をリフレッシュ(水槽全体の掃除、ろ過装置の最新化、ソイルや水草の入れ替え)をするとともに、原因の一つと考えていた照明も変えて、かつリモートでコントロールできるように、その装置を作って運用することにしました。
ちなみに、今まで使っていた水作のLED照明はこんな感じになっています。ろ過装置からの水しぶきに当たっていたせいか、相当のサビがついていました。結構使ったなあーといった感じです。
ちなみに、今まで使っていた水作のLED照明はこちら。
用意したもの
今回はArduinoでWiFi機能が使える「ESP8266 NodeMCU ESP-12E V2U」を使い、Arduinoを使ってコントロールできる「WS2812B」があるLEDテープを使いました。
LEDテープは水槽のフタの上につけますので、少なくとも水しぶき等でパーツがサビてしまう可能性もあったので、防水可能なシリコンカバー付きのものを購入しました。LED間の配置されているピンの接続部分の真ん中部分をハサミで切って使います。
- USB電源アダプタ(5V)
- USBケーブル 0.5m (Type A – micro B)
- 透明のアクリル板(水槽のフタ用)・・・ホームセンターで売っているものを必要に応じて切り取り
- ESP8266を入れるプラスチックケース・・・適当なもの。強いて言えば別のブログで作成したときに使ったハトメのケースがフィットしていたので使用
ブレッドボードでの配置例
おおよそこんな感じで組みました。
シンプルですが、LEDテープからはVCC、GND、デジタルピンがありますので、今回は以下のようにつなげています。
- VCC → 3.3V
- GND → GND
- DO → D8
実装のポイントなど
LEDテープで使用するLED個数は、水槽の大きさを勘案して16個にしました。かつ、照明の明るさの調整ができますが、あまり明るすぎると電流を多く流してしまうことでパーツが壊れるかもしれない心配もあったので、日中の出力を最大の1/2に抑えるようにしました。
リモートで時間帯毎で設定する照明の明るさ、色については、別に用意してあるクラウドのサーバ上で1階のメダカ水槽からの水温データ等を蓄積していたり、グラフ化するためのUIを持っているので、そのコントロールをそのUI(厳密には管理画面)を使って設定情報を登録し、定期的にESP8266からWiFi経由でそのサーバからデータを取得して現在時刻にマッチする時間帯で設定されている情報を適用する形をとっています。
本当は「WebhookRelay」などの外部サービスを使って実現したかったのですが、これらのサービスを使って送信したリクエストをESP8266から取得するためには、Web Sockectの方法で取得する必要がある点、そのリクエストを取得するために、いくつもの認証プロセスを経た上で取得方法となるため、ESP8266にあるWeb Socket用ライブラリの使用上、実現が難しそうだなと思い、今回は作成優先で見送りました。(もし、どなたか実装された方いらっしゃいましたら教えていただければ幸いです)
LEDを使用するAuduinoライブラリは「FastLED」を使いました。
「FastLED」はAuduino IDEから[スケッチ] > [ライブラリをインクルード] >[ライブラリの管理]から「FastLED」を選択してインストール、または直接「FastLED」のサイトからzipファイルをダウンロードし、[スケッチ] > [ライブラリをインクルード] > [zip形式のライブラリをインストール]でも可能です。
他にも「Adafruit NeoPixel」といったライブラリも利用可能のようですが、こちらはインストールして動かしてみたところ、今回のESP8266と相性が良くなかったせいか、うまく動かなかったので、期待通り動いたFastLEDを使っています。
それ以外ではJSON文字列をパース可能な「Auduino Json」を使っています。ライブラリはこちらのリンク先より取得できます。
Arduino IDEからはライブラリマネージャからは「ArduinoJson」を選択してインストールします。
コードを組む時にあらかじめJSON文字列のサイズを見積もって設定しておく必要があるようです。これはサンプルとなるJSON文字列をベースにサイズ確保します。これについてはアシスタントツールがあるようですので、このツールを使ってサイズ見積できます。例えば、次のようにウィザードにしたがって操作を進めると、おおよそ確保すべきサイズを求めることができます。
これをベースに今回の実装に組み込みました。
サンプルコード
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>
#include <time.h>
#include <FastLED.h>
#include <ArduinoJson.h>
#define MAX_NUM_LEDS 16
#define BRIGHTNESS 10
#define DATA_PIN D8
#define JST 3600*9
#define JSON_BUF 4096
const char* ssid = "<ssid>";
const char* password = "<password>";
const char* url = "<設定情報の取得先サーバのエンドポイント>";
// Fingerprint SHA-1, 公開鍵を取得して確認
const uint8_t fingerprint[20] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
CRGB tapeLeds[MAX_NUM_LEDS];
void setup() {
Serial.begin(9600);
connect();
// Initialization
FastLED.addLeds<WS2812B, DATA_PIN, GRB>(tapeLeds, MAX_NUM_LEDS);
Serial.println("Setup LED tape setting...\n");
for(int i = 0; i < MAX_NUM_LEDS; i++){
// Turn the LED on, then pause
tapeLeds[i] = CRGB::Black;
}
FastLED.show();
}
void loop() {
time_t t;
struct tm *tm;
t = time(NULL);
tm = localtime(&t);
Serial.printf("%02d:%02d:%02d\n", tm->tm_hour, tm->tm_min, tm->tm_sec);
refreshLEDLights(tm->tm_hour, tm->tm_min, tm->tm_sec);
delay(60000);
}
void connect() {
// WiFi
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(500);
}
Serial.println();
Serial.printf("Connected, IP address: ");
Serial.println(WiFi.localIP());
configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
}
void refreshLEDLights(int tm_hour, int tm_min, int tm_sec) {
//Wait for WiFi connection
if ((WiFi.status() == WL_CONNECTED)) {
std::unique_ptr<BearSSL::WiFiClientSecure>client(new BearSSL::WiFiClientSecure);
client->setFingerprint(fingerprint);
// もし証明書を無視したい場合はこちらを使用する:
//client->setInsecure();
HTTPClient https;
Serial.print("[HTTP] begin...\n");
if (https.begin(*client, url)) { // HTTPS
Serial.print("[HTTP] GET...\n");
// start connection and send HTTP header
int httpCode = https.GET();
// httpCode will be negative on error
if (httpCode > 0) {
// HTTP header has been send and Server response header has been handled
Serial.printf("[HTTP] GET... code: %d\n", httpCode);
if (httpCode == HTTP_CODE_OK) {
String payload = https.getString();
applyLEDLights(tm_hour, tm_min, tm_sec, payload);
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n", https.getString());
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
}
https.end();
} else {
Serial.printf("[HTTP] Unable to connect\n");
}
} else {
Serial.printf("[HTTP] WiFi is not conntected...\n");
}
}
void applyLEDLights (int tm_hour, int tm_min, int tm_sec, String payload) {
// Deserialize a json payload
// ref: https://arduinojson.org/v6/assistant/
DynamicJsonDocument doc(JSON_BUF);
DeserializationError error = deserializeJson(doc, payload);
if (error) {
Serial.print(F("deserializeJson() failed: "));
Serial.println(error.f_str());
return;
}
int cur_dt = (tm_hour * 10000) + (tm_min * 100) + tm_sec;
boolean is_found = false;
Serial.printf("Checking the following current time: %d\n", cur_dt);
for (JsonObject item : doc.as<JsonArray>()) {
if (!is_found) {
// 以下はJSONオブジェクトから必要なものを取得、あくまでも今回の例
int from_hour = item["from_hour"];
int from_minute = item["from_minute"];
int from_second = item["from_second"];
int to_hour = item["to_hour"];
int to_minute = item["to_minute"];
int to_second = item["to_second"];
int brightness = item["brightness"];
int leds = item["leds"];
int color_r = item["color_r"];
int color_g = item["color_g"];
int color_b = item["color_b"];
// Check if the setting is in range of from-to datetime
int from_dt = (from_hour * 10000) + (from_minute * 100) + from_second;
int to_dt = (to_hour * 10000) + (to_minute * 100) + to_second;
if (from_dt <= cur_dt && cur_dt <= to_dt) {
FastLED.setBrightness(brightness);
if (MAX_NUM_LEDS < leds) {
leds = MAX_NUM_LEDS;
}
int p = 0;
for(int i = 0; i < leds; i++){
tapeLeds[i] = CRGB(color_r, color_g, color_b);
Serial.printf("%d=on ",i);
p = i;
}
// The rest of leds will be changed to Black
for(int i = (p + 1); i < MAX_NUM_LEDS; i++){
tapeLeds[i] = CRGB::Black;
Serial.printf("%d=off ",i);
}
is_found = true;
}
}
}
FastLED.show();
}
取得する設定情報(サンプルJSON)
サーバから受け取るJSONレスポンスの例です。
[
{
"from_hour": 0,
"from_minute": 0,
"from_second": 0,
"to_hour": 6,
"to_minute": 59,
"to_second": 59,
"brightness": 0,
"leds": 16,
"color_r": 0,
"color_g": 0,
"color_b": 0
},
{
"from_hour": 7,
"from_minute": 0,
"from_second": 0,
"to_hour": 7,
"to_minute": 59,
"to_second": 59,
"brightness": 32,
"leds": 16,
"color_r": 255,
"color_g": 255,
"color_b": 255
}
]
実際の組み立て
ESP8266に切り取ったユニバーサル基板にはめ込み、今回つなぐピンとケーブルとをはんだ付けしました。
ESP8266自体を水槽の水から保護するために、適当なプラスチックケース(機器から出る熱を考慮し、数カ所穴あけ)を用意し、その中にESP8266を入れて、USBケーブルで繋ぎました。
作成したもの
最終的にこんな感じで作成し、実際のメダカの水槽に設置しました。
ESP8266が定期的に設定情報が格納されているサーバからデータを取得して、現在時刻にマッチする時間帯のある情報を使ってLED照明の明るさや色をコントロールしています。
設定情報があるサーバについては説明するとキリがないので詳細は割愛しますが、Djangoで構築しているサーバにある情報を取得しています。例えば、1行目のレコードでは、0時から7時までは照明をOFF、2行目のレコードでは、7時から8時までは照明の明るさ設定(brightness)を32に調整しています。
実際の動作例はこんな感じです。
定期的にチェックする現在時刻をもとに、サーバから取ってきた設定情報をチェックして、マッチした時間帯の設定情報を使ってLED照明をコントロールできるようになりました。以下の動画では、チェックのタイミングで設定情報が変わったため、LED照明が自動的に白 → 緑 → 白に変わりました。
振り返り
とりあえず、リモートコントロールできるLED照明を使ってメダカの水槽の照明コントロールができるようになりました。これで、朝、夜にいちいち照明をON/OFFすることなく、勝手に照明がつく、消すことができるようになりました。
時間帯によって徐々に照明が明るくなる、暗くなるように設定しているので、メダカ自身のストレスもおそらく減るのかな。。。
とはいえ、細かい課題(というか、原因がわからない。。。)がいくつかあり、例えば、設定したプログラムにミスがあるのか、どうも端っこのLEDだけが設定していない色(今回は水色っぽい色)が期待している色(白色)に変更されず、常に表示されているんですよね。setup時に初期化で全部OFFにしているつもりなのですが、セットアップ後は白色なのですが、しばらくするとなぜかその色になってしまうんですよね。
今後やりたいこと
今回なぜ、1つだけLEDの表示がおかしいのかは調べるとして、次回のタイミングで改善するのであれば、やっぱり自分から都度取りに行くのではなく、WebhookRelayなどを使って取得できるようにしたいなと思います。これを満たせるようなライブラリが転がっているんですかねぇ。。。