前回、重量センサーの値をサーバーへアップロードすることができたので、今度はにおいセンサーをインストールします。
回路は以前の記事の2SC1815を使ったものをそのまま採用
構想
においセンサー付きの猫用スマートトイレは見たことがないので世界初なのではないでしょうか?(しらんけど)
実際には今からデータを蓄積して運用しながら解析して使えるかどうか判断することになりますが、においセンサーを付けることにより、
- 重量変化の分析だけより糞尿の識別が簡単になる(猫のうんこは臭い)
- 便の臭気を可視化することで体調管理に役立つ
と考えていますが、はたしてうまくいくかどうか。。
重量センサー、においセンサー、インジケータLEDを搭載した基板がこちら。
ESP32モジュールに元々付いていたピンは撤去して上から線を伸ばして基板へ配置。こうすることで将来ESP32本体の取替が必要になった場合にも線を切るだけで容易に交換することができます。本体も少しだけですが薄くすることができます。
重量計へ接続したらこんな感じ。USB給電が必要ですが装置はとてもシンプルです。
重量計と基板を分離したのは、においを検知しやすいトイレの上側ににおいセンサーを設置するためです。
センサーへはESP32の排熱を利用して吸気させることにします。
外観
ケージに取り付けるとこのように。
トイレを乗せてみました。
あまりにも違和感がないので、家族のお出かけ中に設置しましたが誰にも突っ込まれませんでした。
コード全容
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
const char *ssid = "【ルーターSSID】";
const char *password = 【ルーターパスワード】";
// 計測結果エンドポイントURL
const char *uploadWeightURL = "【体重アップロードURL】";
const char *uploadOdorURL = "【においアップロードURL】";
void AE_HX711_Init(void);
void AE_HX711_Reset(void);
long AE_HX711_Read(void);
long AE_HX711_Averaging(long adc,char num);
float AE_HX711_getGram(char num);
//体重計測定値のストック
const int maxWeightStockSize = 300; // 60s * 5min
int weightArray[maxWeightStockSize]; // 配列
int currentWeightIndex = 0; // 現在のインデックス
int cntNosend = 0; //データ送信フラグをスルーするカウント
int level_weight = 1500; //体重計反応レベル//におい測定値のストック
const int maxOdorStockSize = 300; // 0.5s
int odorArray[maxOdorStockSize]; // 配列
int currentOdorIndex = 0; // 現在のインデックス
int cntOdorNosend = 0; //データ送信フラグをスルーするカウント
bool isOdorWrite = false; //データ書き込み中フラグ
float level_odor = 0.007; //匂い反応レベル 0.01 反応悪い 0.005 反応しすぎint error_send = 0; //送信失回数カウント
int wd_cnt = 0; //システムエラー監視カウンター
//重量センサ
#define pin_dout 27 //yellow
#define pin_slk 26 //white
//においセンサ 元がプルダウンのピンだけ使う
#define TGS_heat 2
#define TGS_pw 4
#define TGS_sens 33
//インジケータLED
#define LED_1 18 //ニオイセンサー反応時点滅
#define LED_2 19 ////---------------------------------------------------//
// ロードセル シングルポイント( ビーム型) 50kG*4
//---------------------------------------------------//
#define OUT_VOL 0.001f //定格出力 [V]
#define LOAD 200000.0f //定格容量 [g]float offset;
//マルチスレッドのタスクハンドル格納用 ニオイセンサー ウォッチドッグ
TaskHandle_t thp[2];/* 蓄積した重量データをjson形式へ変換 */
String getJsonOfWeight() {
DynamicJsonDocument jsonDocument(5120); // JSONデータの最大サイズを指定 int 4bite*300= 1200 1024 2048 4096 5120
JsonObject postData = jsonDocument.to<JsonObject>();
JsonArray dataArray = postData.createNestedArray("W");
for (int i = 0; i < currentWeightIndex; i++) {
dataArray.add(weightArray[i]);
}
String jsonData;
serializeJson(postData, jsonData);
return jsonData;
}/* 蓄積したにおいデータをjson形式へ変換 */
String getJsonOfOdor() {
//別スレッドで配列にデータ書き込み中の場合は待つ
while( isOdorWrite ){
delay(1);
}
DynamicJsonDocument jsonDocument(5120); // JSONデータの最大サイズを指定 int 4bite*300= 1200 1024 2048 4096 5120
JsonObject postData = jsonDocument.to<JsonObject>();
JsonArray dataArray = postData.createNestedArray("O");
for (int i = 0; i < currentOdorIndex; i++) {
dataArray.add(odorArray[i]);
}
String jsonData;
serializeJson(postData, jsonData);
return jsonData;
}/* jsonデータをPOSTでサーバーへ送る 失敗時エラーLED */
void sendPostJsonData(String targetURL, String jsonData){
for(int i = 0; i < 3; i++){
HTTPClient http;
http.setTimeout(10000);
http.begin(targetURL);
http.addHeader("Content-Type", "application/json");
int httpResponseCode = http.POST(jsonData);
delay(200);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println("HTTP Response: " + response);
error_send = 0;
http.end();
break;
} else {
Serial.print("HTTP Error: ");
Serial.println(httpResponseCode);
error_send++;
//3回分送信失敗したらESPを再起動
if( 9 <= error_send){
Serial.println("ESP-Restart");
ESP.restart();
}
http.end();
delay(5000);
}
}
delay(1000);
}/* 蓄積した計量をサーバーへ送る */
void sendWeight(){
sendPostJsonData( uploadWeightURL ,getJsonOfWeight() );
}
/* 蓄積した臭度をサーバーへ送る */
void sendOdor(){
sendPostJsonData( uploadOdorURL ,getJsonOfOdor() );
}/* 重量データを配列の最後に追加更新 */
void addToWeightArray(int newValue) {
if (currentWeightIndex < maxWeightStockSize) {
// 配列の要素数が指定サイズ未満の場合はそのまま追加
weightArray[currentWeightIndex] = newValue;
currentWeightIndex++;
} else {
// 配列の要素数が指定サイズ以上の場合は最初の値を削除して追加
for (int i = 1; i < maxWeightStockSize; i++) {
weightArray[i - 1] = weightArray[i];
}
weightArray[maxWeightStockSize - 1] = newValue;
}
}/* wifiの接続 接続完了時にIPアドレスをサーバーへ送る */
void connectToWiFi() {
WiFi.begin(ssid, password);
WiFi.setSleep(false); // WiFiスリープを無効にするなどの設定を行う場合は追加// WiFi接続が確立されるまで待機
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}// WiFi接続が確立した場合の処理
Serial.println("");
Serial.println("WiFi connected");
Serial.printf("IP Address ");
Serial.println(WiFi.localIP());
//ローカルIPアドレスを送信することでサーバー側でも起動確認させる
//String jsonData = "{\"I\":\"" + WiFi.localIP().toString().c_str() +"\"}";
String jsonData = "{\"IP\":\"" + WiFi.localIP().toString() + "\"}";
sendPostJsonData( uploadWeightURL , jsonData);
}/* においセンサーヒーター制御 */
void heatTGS(){
digitalWrite(TGS_heat, HIGH);
delay(8);
digitalWrite(TGS_heat, LOW);
delay(242);
}
/* ニオイセンサー値取得 */
int readTGS(){
heatTGS();
digitalWrite(TGS_pw, HIGH);
delay(5);
int sensorValue = analogRead(TGS_sens); //12bit 4096step
digitalWrite(TGS_pw, LOW);
delay(245);
return sensorValue;
}/* ニオイセンサ 配列の最後に追加して更新 */
void addToOdorArray(int newValue) {
isOdorWrite = true;
if (currentOdorIndex < maxOdorStockSize) {
// 配列の要素数が指定サイズ未満の場合はそのまま追加
odorArray[currentOdorIndex] = newValue;
currentOdorIndex++;
} else {
// 配列の要素数が指定サイズ以上の場合は最初の値を削除して追加
for (int i = 1; i < maxOdorStockSize; i++) {
odorArray[i - 1] = odorArray[i];
}
odorArray[maxOdorStockSize - 1] = newValue;
}
isOdorWrite = false;
}/* マルチスレッドでにおいセンサー制御&取得(常にヒーターが必要なため) */
void monitorTGS(void *args){
while(1){
int valOdor = readTGS();
addToOdorArray( valOdor );
Serial.print("valOdor :");
Serial.print( valOdor );
Serial.print(" index:");
Serial.println( currentOdorIndex );
delay(1);
}
}/* システムエラー(ループ内異常)で再起動 */
void watchDog(void *args){
while(1){
if( 60 < wd_cnt){
ESP.restart();
}
delay(1000);
wd_cnt++;
}
}/* 体重計,ニオイセンサー,LED,WiFi,Serial */
void setup() {
//ニオイセンサー準備
pinMode(TGS_heat, OUTPUT);
pinMode(TGS_pw, OUTPUT);
pinMode(TGS_sens, INPUT);
digitalWrite(TGS_heat, LOW);
digitalWrite(TGS_pw, LOW);
//マルチスレッドでニオイセンサー開始
xTaskCreatePinnedToCore( monitorTGS , "monitorTGS", 4096, NULL, 4, &thp[0], 1); // 優先度0最低 25最大
//システムエラー監視
xTaskCreatePinnedToCore( watchDog , "watchDog", 1024, NULL, 4, &thp[1], 1); // 優先度0最低 25最大
Serial.begin(115200);pinMode(LED_1, OUTPUT);
pinMode(LED_2, OUTPUT);
digitalWrite(LED_1, LOW);
digitalWrite(LED_2, LOW);connectToWiFi();
Serial.println("AE_HX711 Init");
AE_HX711_Init();
AE_HX711_Reset();
delay(1000);
offset = AE_HX711_getGram(20);String jsonData = "{\"msg:\":\"init-OK\"}";
sendPostJsonData( uploadWeightURL , jsonData);
// Serial.println("offset : ");
// Serial.println( offset );
}/* 値の変化でサーバーへデータを送る 減ったタイミング */
bool checkWeight(){
if( 0 < cntNosend ){
cntNosend-- ;
}else if (currentWeightIndex +1 >= maxWeightStockSize) {
// 配列の要素数が指定サイズ以上
//10秒前より5秒前のほうが1.5Kg以上軽い(猫がトイレから出た、トイレ掃除とか..この辺はサーバー側で処理させる)
if( weightArray[currentWeightIndex -10] - weightArray[currentWeightIndex -5] > level_weight ){
//連続してデータを送らないようにカウント中はこのチェックを無視する
cntNosend = 10;
return true;
}
}
return false;
}/* 臭気値の変化でサーバーへデータを送る */
bool checkOdor(){
if( 0 < cntOdorNosend ){
cntOdorNosend-- ;
//反応したらLED交互点滅
digitalWrite(LED_1, !digitalRead(LED_1) );
digitalWrite(LED_2, !digitalRead(LED_1) );
// 配列の要素数が指定サイズ以上
}else if(currentOdorIndex +1 >= maxOdorStockSize) {
//ニオイセンサーエラー
if( odorArray[0] == 0 && odorArray[currentOdorIndex] == 0 ){
digitalWrite(LED_1, !digitalRead(LED_1) );
digitalWrite(LED_2, !digitalRead(LED_2) );
}else{
//90秒前より30秒前のほうが1%ぐらい下がる? (1計測/0.5秒 反応みて閾値変更)
if( odorArray[currentOdorIndex -180] - odorArray[currentOdorIndex -60] > (int)( odorArray[currentOdorIndex -180] * level_odor ) ){
Serial.println("OdorSens ->send");
//連続してデータを送らないようにカウント中はこのチェックを無視する
cntOdorNosend = 180;
return true;
}
digitalWrite(LED_1, LOW);
digitalWrite(LED_2, LOW);//送信失敗したら点灯
if( 6 <= error_send){
digitalWrite(LED_2, HIGH);
}else if( 3 <= error_send){
digitalWrite(LED_1, HIGH);
}
}
}
return false;
}/* 重量取得し、必要時に重量、においセンサ値をサーバーへ送る 約1秒周期 */
void loop() {int data = (int)AE_HX711_getGram(8); //平均化で8+2の10回取得する
addToWeightArray( data ); //オフセット無しの値//値の確認用なのでオフセット引く
String S1 = String(data - (int)offset);
Serial.print("g:");
Serial.print(S1); // Print the String to the serial monitor
//Serial.print(",data:");
//Serial.print( String(data) );
//Serial.print(",offset:");
//Serial.println( String( offset ) );
Serial.print(" stock:");
Serial.println( String( currentWeightIndex ) );if( checkWeight() ){
sendWeight();
}//delay(500);
if( checkOdor() ){
sendOdor();
}
delay(1);
// WiFi接続が失われた場合に再接続を試みる
if (WiFi.status() != WL_CONNECTED) {
connectToWiFi();
}
//きちんとループが回っているとウォッチドッグカウントをリセット
wd_cnt = 0;}
/* 重量センサピン初期化 */
void AE_HX711_Init(void)
{
pinMode(pin_slk, OUTPUT);
pinMode(pin_dout, INPUT);
}
/* 重量センサリセット */
void AE_HX711_Reset(void)
{
digitalWrite(pin_slk,1);
delayMicroseconds(100);
digitalWrite(pin_slk,0);
delayMicroseconds(100);
}
/* 重量センサ値取得 */
long AE_HX711_Read(void)
{
long data=0;
while(digitalRead(pin_dout)!=0);
delayMicroseconds(10);
for(int i=0;i<24;i++)
{
digitalWrite(pin_slk,1);
delayMicroseconds(5);
digitalWrite(pin_slk,0);
delayMicroseconds(5);
data = (data<<1)|(digitalRead(pin_dout));
}
//Serial.println(data,HEX);
digitalWrite(pin_slk,1);
delayMicroseconds(10);
digitalWrite(pin_slk,0);
delayMicroseconds(10);
return data^0x800000;
}/* 平均化した値を取得 計測数+2で計測して最大と最小を除外する。 */
long AE_HX711_Averaging(char num)
{
long sum = 0;// 最大値と最小値を保存
long maxVol = 0;
long minVol = 0;for (int i = 0; i < num + 2; i++)
{
long vol = AE_HX711_Read();
if ( i == 0 ){
maxVol = vol;
minVol = vol;
}if (vol > maxVol)
{
maxVol = vol;
}
if (vol < minVol)
{
minVol = vol;
}sum += vol;
}// 最大値と最小値を除いた合計を計算
sum -= maxVol + minVol;// 最大値と最小値を除いた合計を指定数で割って平均を返す
return sum / num;
}/* センサ値の平均値をグラム換算して取得 */
float AE_HX711_getGram(char num)
{
//基準電圧の分圧 HX711のデータシートから 20Kと8.2Kの抵抗分圧
#define HX711_R1 20000.0f
#define HX711_R2 8200.0f
//内部基準電圧(バンドギャップ電圧)。一般的には固定値
//5V=1.25とあるので、印加電圧の1/4? 3.3V/4 =0.825?
//#define HX711_VBG 1.25f <- 元データ
#define HX711_VBG 0.825f
//HX711_AVDD = 0.825 * ( ( 20000 + 8200.0) / 8200.0 ) = 2.83719512195121
//#define HX711_AVDD 4.2987f//(HX711_VBG * ( ( HX711_R1+HX711_R2)/HX711_R2)) <-元データ
#define HX711_AVDD 2.83719//1ビットあたりの電圧を表す。アナログ入力値を電圧に変換する際に使用
#define HX711_ADC1bit HX711_AVDD/16777216 //16777216=(2^24)
//プログラマブル・ゲイン・アンプ(PGA)のゲイン設定を表す。PGAはアナログ入力信号の増幅に使用され、正確な測定範囲を調整
#define HX711_PGA 128
//ADCから得られたデータをロードセルの重さに変換するためのスケーリングファクター。LOAD はロードセルの定格負荷。
//このスケーリングファクターによって、ADC値がロードセルの重さにどのように対応するかが計算される。
#define HX711_SCALE (OUT_VOL * HX711_AVDD / LOAD *HX711_PGA )
float data;
//指定回数だけADC値を読み取り、その平均値を計算
//data = AE_HX711_Averaging(AE_HX711_Read(),num)*HX711_ADC1bit;
data = AE_HX711_Averaging( num )*HX711_ADC1bit;
//Serial.println( data);
data = (data / HX711_SCALE )/2; //何故か2倍の値が出てたので1/2とするreturn data;
}
動作概要
- 起動したら体重アップロード用URLへ自身のローカルIPアドレスを送信
- 重量が1.5kg減ったら過去300計測データを送信
- 臭気センサ値が0.7%反応があれば過去300計測データを送信してLED点滅
以上!動作はめちゃ単純。
ついでにWiFi接続エラーで再接続をさせ、内部ループエラー(フリーズ60秒)で再起動させ、送信エラーが3回続くと再起動させるという機能も含めています。
実際にデータを蓄積しながら、サーバーでWebアプリを作りながら、不具合を修正ながら運用をしていきます。
考察
テストでは便の場合、臭気センサーの値が
2663 → 2545
ぐらいに変化してましたが、反応が鈍い場合もあるので運用しながら試していかないといけません。
また、猫トイレの砂や部屋の環境を変えると値も変わるかもしれません。(現在は割と消臭能力の高い猫砂を使用)
おまけで匂いを感知するとLEDが点滅するようにしました。視覚的にトイレ掃除を促せます。
しかし、市販されているCatlogBoardはこのサイズで電池駆動で長期運用可能だというのですから驚きです。作ってみて解る市販品の凄さですね。
続き