zunya が 2021年02月26日12時00分24秒 に編集
初版
タイトルの変更
ウォーターサーバのボトル交換時期をslackでお知らせ
タグの変更
秋葉原2021
ESP-WROOM-02
slack
ロードセル
WiFi
メイン画像の変更
記事種類の変更
製作品
本文の変更
作るきっかけ - ウォータサーバの水ボトルが空になっても交換されずに放置され、カップ麺にお湯を注ぎ始めた直後に気付いて悲しい気持ちになった為作りました。 仕組みの検討 - 光センサ等で水位を観測すれば良いかなと始めは考えていましたが、ウォーターサーバ本体と水ボトルはレンタル品の為加工することができず…。ならば体重計の様な感じのものを作って、ウォータサーバごと(装置に載せて)重さを測り、水ボトル満タン時の重さと、空の時の重さを事前に記録しておけば大まかな残量が把握できるのではないか…という事で仕組みとしては図のようにすることとしました。 ![キャプションを入力できます](https://camo.elchika.com/92129751b2d1216c21989c4cb7be1710794f8411/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f34656531663430302d346264652d346239612d613837382d326436643235316138633138/) ロードセルは4つありますが、実際の測定に使っているのはその内の1つだけです(残り3つは足として使うだけ)。 用意するもの - - ESP-WROOM-02モジュール(秋月電子のAE-ESP-WROOM02-DEV [K-12236]) - HX711モジュール(秋月電子のAE-HX711-SIP [K-12370]) - ロードセル(Wii Fit バランスボードから外したもの) 1セット(4個) - 抵抗器 2.7kΩ, 4.7kΩ いずれも1/6W(小型品)それぞれ1本ずつ - MDF材 600x300x2.5 2枚 - 他 線材, 木工用ボンド,両面テープ, はんだ, USBケーブル(microB-A), USBmicroBコネクタ付きACアダプタ…等 ※ 加えて、工具が必要です(はんだゴテ、レーザ加工機、+ドライバ、ニッパー等)。 回路 - HX711モジュールはIC内部レギュレータを使用するので5V動作としました。 ターミナルブロックなどは使用せず、直接電線で配線しました。 使用したESP-WROOM-02は3.3V動作なのでそのまま接続することは避け、一応簡易なレベル変換を付加しました(抵抗2本)。 ![回路図](https://camo.elchika.com/1cc6c63fea16b301be53225ff24fcb56dd8a02db/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f32323731336364662d656339352d343739662d623432622d336661643038373062666231/) ウォータサーバを載せる部分 - ウォータサーバは満タンの水ボトルを含めて約20kg程(おそらく)あるので、その重さに耐えられなければなりません。重心が高くなると転倒の恐れもある為、なるべく薄型としました。 2.5mmのMDF材が手元にあったので、それをレーザー加工機で切り出し木工用ボンドで積層・接着しました。ウォータサーバの足がロードセルの真上に来るようにすることで、薄い板でも何とかなりました。ロードセルは、Wii Fitバランスボードを分解して外したものを使用しました(ネジとプラスチックの足も再利用)。プラスチックの足は、ロードセルに両面テープを使って簡易に固定しておきました。 ![MDF切り出し後に積層接着したところ](https://camo.elchika.com/2204f7e12321892452623ef35dedaba9f6c67922/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f30633163336335342d626337612d346138642d393035322d306461663563306235326231/) ![ロードセルを窪みに嵌めてからネジ止めし、両面テープ貼り付け](https://camo.elchika.com/b471b4809574f7c28229ef6bba8b52d3c5ce1796/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f35653631336533362d346534362d343638332d623435332d343362633935653866343338/) ![4か所同様に取り付けて、最後に足を付ける](https://camo.elchika.com/8170f3105f8abb001f5c8209f6c1bcc986d391e9/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f66363263656433642d656539662d343039632d383538312d643530323436653764666231/) ![WROOM-02基板とHX711基板を配線](https://camo.elchika.com/0c95dff96078b38ab5140c9f7f516f0d43ff9259/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f39663964373037322d633531652d343462642d626439392d656333353731313665343234/) とりあえず、ここまででハードは完成です。 WROOM-02のArduino化とslackの設定 - こちらの記事を参考にさせていただきました。 https://elchika.com/article/6b591591-dd4f-44ec-ae37-e77a8ffcee51/ slackポストで使うアイコンは適当に描きました。 ![アイコン](https://camo.elchika.com/dd24eaa20289d76b746973793fab76e4b61de165/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f64373761656232302d386534622d346235312d396532352d353134643766616230356336/) スケッチ(コード)など - 水タンクの大よその残量が分かれば良いので、空のボトルをセットしたときと満タンボトル時のHX711の出力をそれぞれ記録し、その値を基に現在の予想ボトル水位を算出します。加えて、slackへのポスト、(疑似)EEPROMへのデータ保存と読み込みなどなどのコードが盛り込まれています。コード中のXXXXXは環境に合わせてください(SSIDなど)。 ``` #include <ESP8266WiFi.h> #include <WiFiClientSecure.h> #include <WiFiUdp.h> #include <ESP8266WebServer.h> #include <ESP8266mDNS.h> #include <EEPROM.h> // WiFi設定 static const String ssid = "XXXXX"; static const String password = "XXXXX"; // slackポスト関連 static const String SLACK_HOST = "hooks.slack.com"; static const String SLACK_URL = "XXXXX"; static const int PORT = 443; static const char* FINGERPRINT = "XXXXX"; // HX711関連 static const int pinDAT=16; // HX711のDATピンに接続されているWROOM-02のピン static const int pinCLK=5; // HX711のCLKピンに接続されているWROOM-02のピン // ウォータサーバ監視関連 static const int msgint = 15; // 定時連絡は15分おき static const int wlbtlempty = 5; // 5%切ったら空判定 static const int wlnewbtl = 95; // 空判定後、95%以上になったらボトル交換されたと判定 // (疑似)EEPROM用データ定義 struct EEPROM_CONFIG{ long wlfull; long wlempty; }; EEPROM_CONFIG epcfg; // 設定値が"0"(未設定)の場合にセットするデフォルト値 static const long wlfull_default = 9200000; static const long wlempty_default = 9100000; // ブラウザから現在値などを確認できるようにウェブサーバを追加 ESP8266WebServer server(80); // (疑似)EEPROM 読み書きルーチン void load_eeprom() { EEPROM.get<EEPROM_CONFIG>(0, epcfg); // 設定値が"0"(未設定)の場合はデフォルト値をセットしておく if(!epcfg.wlfull) epcfg.wlfull=wlfull_default; if(!epcfg.wlempty) epcfg.wlempty=wlempty_default; } void save_eeprom() { EEPROM.put<EEPROM_CONFIG>(0, epcfg); EEPROM.commit(); } // slackにmsgに入れた文字列をポストする void submit_slack(String msg) { WiFiClientSecure client; client.setFingerprint(FINGERPRINT); if (!client.connect(SLACK_HOST, PORT)) { Serial.println("Slackとの接続に失敗しました"); } String payload = "payload={\"text\": \"" + msg + "\"}"; Serial.println(payload.c_str()); String request = "POST " + SLACK_URL + " HTTP/1.1\n" + "Host: " + SLACK_HOST + "\n" + "User-Agent: ESP8266\n" + "Connection: close\n" + "Content-Type: application/x-www-form-urlencoded\n" + "Content-Length: " + payload.length() + "\n\n" + payload; client.println(request); delay(5000); while (client.available()) { String line = client.readStringUntil('\r'); Serial.print(line); } Serial.println(""); client.stop(); } // 現在のHX711の値からおおよその水残量(%)を計算する int getwaterlevel(void) { long hx711val=AE_HX711_Read(); return((float(hx711val - epcfg.wlempty)/float(epcfg.wlfull - epcfg.wlempty))*100); } // HX711関連ルーチン void AE_HX711_Init(void) { pinMode(pinDAT, INPUT); pinMode(pinCLK, OUTPUT); } void AE_HX711_Reset(void) { digitalWrite(pinCLK,1); delayMicroseconds(100); digitalWrite(pinCLK,0); delayMicroseconds(100); } long AE_HX711_Read(void) { long data=0; while(digitalRead(pinDAT)!=0); delayMicroseconds(10); for(int i=0;i<24;i++) { digitalWrite(pinCLK,1); delayMicroseconds(5); digitalWrite(pinCLK,0); delayMicroseconds(5); data = (data<<1)|(digitalRead(pinDAT)); } digitalWrite(pinCLK,1); delayMicroseconds(10); digitalWrite(pinCLK,0); delayMicroseconds(10); return data^0x800000; } // ブラウザで"/"にアクセスしたときに出力する void handle_root() { long hx711val=AE_HX711_Read(); String message = "Water Server Status"; message += "\nCurrent="; message += String(hx711val); message += " ("; message += String(getwaterlevel()); message += "%)"; message += "\nFull="; message += String(epcfg.wlfull); message += "\nEmpty="; message += String(epcfg.wlempty); server.send(200, "text/plain", message); //↓のコメントアウトを解除すると、"/"にアクセスするたびにslackへポストします. //submit_slack(message); } // ブラウザで存在しないページにアクセスされたときに出力する void handle_notfound() { String message = "File Not Found\n\n"; message += "URI: "; message += server.uri(); message += "\nMethod: "; message += (server.method() == HTTP_GET) ? "GET" : "POST"; message += "\nArguments: "; message += server.args(); message += "\n"; for (uint8_t i = 0; i < server.args(); i++) { message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; } server.send(404, "text/plain", message); } // 約1秒おきに呼ばれるルーチン // 60カウントして1分として、水位確認を実施. // さらに15分おきに定時連絡としてslackに現在水位をポスト. void tmr_wlcheck(void) { static int scnt=0; static int mcnt=0; static bool flg_empty=false; Serial.println("scnt=" + String(scnt)); scnt++; if(scnt>=60){ int wlpercent = getwaterlevel(); Serial.println(String(wlpercent) + "%"); if((wlpercent < wlbtlempty) && !flg_empty){ String message; message+="ボトルが空になったみたいです.\n"; message+="ボトル交換してください.\n"; submit_slack(message); flg_empty=true; } if((wlpercent > wlnewbtl) && flg_empty){ String message; message+="ボトルが交換されました.\n"; submit_slack(message); flg_empty=false; } scnt=0; mcnt++; if(mcnt>=msgint){ String message; message+="ボトル水残量の定時連絡です.\n"; message+="現在、" + String(wlpercent) + "%です.\n"; submit_slack(message); mcnt=0; } } } // もろもろ初期化 void setup(void) { AE_HX711_Init(); EEPROM.begin(64); load_eeprom(); Serial.begin(115200); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.println(""); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.print("Connected to "); Serial.println(ssid); Serial.print("IP address: "); Serial.println(WiFi.localIP()); if (MDNS.begin("esp8266")) { Serial.println("MDNS responder started"); } server.on("/", handle_root); server.on("/full", []() { long hx711val=AE_HX711_Read(); epcfg.wlfull=hx711val; save_eeprom(); server.send(200, "text/plain", "Full=" + String(hx711val)); }); server.on("/empty", []() { long hx711val=AE_HX711_Read(); epcfg.wlempty=hx711val; save_eeprom(); server.send(200, "text/plain", "Empty=" + String(hx711val)); }); server.onNotFound(handle_notfound); server.begin(); Serial.println("HTTP server started"); } // ループ void loop(void) { server.handleClient(); MDNS.update(); tmr_wlcheck(); delay(1000); yield(); } ``` スケッチの書き込み方などは省略します。 設置 - 写真のようにウォータサーバの下に敷きます。 なるべくロードセルの真上にウォータサーバの足が来るように載せます。 ![完成した本体に…](https://camo.elchika.com/809966e63a6d8201ded629a8d0361ffe55fa1087/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f36663862653864612d363666662d343263312d386230332d353531323232376535633663/) ![ウォータサーバを載せます](https://camo.elchika.com/06c5c8b108c5049af49b26318b5e945e3a07bbf4/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f66373733663639322d616165652d346466322d616433652d646137306630646332636630/) あとは、USBmicroBコネクタ付きのACアダプタなどで適当に給電します。 動作テスト - まず、"esp8266.local"(または割り当てられたIPアドレスを調べて入力)にアクセスしてHX711での測定値を確認します。 ``` Water Server Status Current=9147730 (48%) Full=9200000 Empty=9100000 ``` ※このような感じの表示になるはずです(Current値は状況による)。 次に、空のボトルをセットしてから、"esp8266.local/empty"にアクセスして空ボトルの値を保存します。続けて、満タンのボトルに替えて、"esp8266.local/full"にアクセスし、満タン時の値を保存します。 再度、"esp8266.local"にアクセスして設定値を確認します。 ``` Water Server Status Current=9165397(100%) Full=9165397 Empty=9116586 ``` ※このような感じの表示になるはずです(値は状況によりますが、99~101%くらいの表示になるはずです)。 これで設定は完了です。 およそ15分に一回、水ボトル残量をslackにポストします。 ![水残量の定時連絡](https://camo.elchika.com/fdb9a9c1c24cabbcacac244d164adee7816d8b63/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f38313664386332622d656330322d343562642d613736642d633065636237346261623838/) ボトルの水が無くなると… ![ボトル交換時期の通知](https://camo.elchika.com/e902ed6c6aa5ee1e2675f2d5b7b1b4653a0fd2c5/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f38376432333963352d326366322d346637372d623665312d633237386135393639396135/) ボトルが交換されると… ![ボトル交換完了の通知](https://camo.elchika.com/7a19a8b54c395fc29d28a8a744d181dae5843ef6/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f36313331336636652d383636612d343634612d623334622d3262663962353764323438652f35633863376436362d383332372d343331612d386364372d303065326531396465653539/) と言った具合にお知らせしてくれます。 とりあえず完成 - 水タンクの残量をslackで確認することができるようになり、これで悲しい思いをすることがなくなりそうです!(空であることを知っててボトル交換をスルーしないことが前提ですが…) 手抜きでロードセルを1ch分しか測定していない為、水を出す時のレバー操作などを誤検出したり、ウォータサーバ本体を載せ直すなどすると基準値の取り直しが必要になったりと…まだまだ改良の余地はありそうです。インターフェースをテキストからHTMLにするというのもありますね。 今更ですが、カップ麺に注ぎ始める前にボトル残量目視すれば済む話でした…。