DreamerDreamのブログ

夢想家の夢です。〜揚げたてのモヤっとしたものをラフレシアと共に〜

猫の体重管理のためにスマートトイレを自作してみた ⑥送られてきたデータをサーバーで処理する

前回ESP32でサーバーへ重量データと臭気データを送ることに成功しました。

dreamerdream.hateblo.jp

 

今回はサーバーに送られてきた重量変化のデータから

  • 体重
  • 排泄物計量
  • トイレ滞在時間

を分析するコードを書きました。

コードの概要

  1. 重量変化を降順に並べ、3回計測分の平均値を重量とします(体重計側で8回分の平均を出しているので、正確には24回計測分の平均値)。
  2. 最後が猫が降りた後の重量を基準として、1.5Kg以上の重さがあり、且つ3秒以内の誤差が4g以下(最初20gからいろいろ試して最終的に4gになりました)の場合に用を足している最中の静止状態として猫の体重とします。
  3. 猫の体重が求められたら、猫の体重から1.5kg以上減り、且つ計測器が1g以内の誤差で静止している場所を初期値(オフセット値)として記録します。
  4. 基準値から初期値を引いたら排泄量が求められます。
  5. 初期値が求められるまでの間をカウントし、トイレ滞在時間とします。

コード

データは

data = [ 369377, 369357, 369376,....(300個ぐらいのint値)..366694]

このようなオフセット値無しの生データの配列です。

 

#重量データから猫の排泄状況解析。エラーの場合、offsetが0になる

def analyse_weight( datas  ):

  cnt = 0

  w_stock = []

  w_offset = 0 #オフセット

  w_pee = 0 #降りたときの値

  w_weight = 0 #乗って落ち着いたときの重量

  cnt_stay = 0 #滞在時間

 

  for w in reversed( weights ):

    #3つの数値の平均値

    w_stock.append( w )

    w_stock = w_stock[-3:]

    avl = statistics.mean( w_stock )

 

    #最後から3つまでを降りたときの重量とする

    if cnt == 3:

      w_pee = avl

 

    if w_pee != 0 and w_weight == 0:

      #降りた時の重量より1500g以上重く、3秒間誤差が4g以内に収まっている状態

      if w_pee +1500 < avl:

        if cnt_stay == 0:

          cnt_stay = cnt #滞在中の時間計測用

        if w_stock[0]-2 < w_stock[1] and w_stock[1] < w_stock[0]+2 and w_stock[1]-2 < w_stock[2] and w_stock[2] < w_stock[1]+2:

          w_weight = avl

 

    #乗る前の重量なので、3秒間で+=1gの静止時で、1500g以上の重量変化の前

    if w_weight != 0 and w_offset == 0 and w_weight -1500 > avl:

      if w_stock[0]-1 < w_stock[1] and w_stock[1] < w_stock[0]+1 and w_stock[1]-1 < w_stock[2] and w_stock[2] < w_stock[1]+1:

        w_offset = avl

        cnt_stay = cnt - cnt_stay

        break

 

    cnt+=1

 

  #オフセット値を引く

  w_pee = w_pee -w_offset

  w_weight = w_weight -w_offset

 

  res = {'offset':w_offset ,'pee': int(w_pee), 'weight': int(w_weight) ,'Stay': cnt_stay }

 

  return res

 

 

トイレ掃除などの場合はoffset値が0となり、猫が乗って排泄して降りるという行為が5分以内に行われた場合には正常に機能します。

マイナス値や一桁値はおそらくトイレに入っただけだと思います。(トイレ砂が出た量?)

オフセット値は都度変化しています。これは、

  • 室温の影響
  • 体重計(木材)の変形
  • 排泄物の蒸発
  • トイレ砂の増減
  • 掃除

など理由は多岐にわたりますが、重量の増減前の静止時の値をオフセット値としているので問題ありません。

そして想像以上に体重が正確に出ていることに驚きました。排泄後に食事無しで入った場合、しっかりと排泄分が減っています。

10g程度は誤差があると思いますが、このロードセルは安いしかなり優秀ではないでしょうか。

dreamerdream.hateblo.jp

 

次に、排泄時の重量変化をグラフ化してみました。

 

大と小

結構特徴的なグラフになりますね。

トイレの滞在時間だけでも違いますが、静止(排泄行為)の後のネコババ行為の長さが圧倒的に違うので、ここだけでも割と判断ができそうです。

 

排泄行為後の長さをbabaとして集計しました。

大凡ですが、1分以上排泄後に滞在している場合はほぼ確実に大だと推測することができます。

両方の場合はもう少し良いアルゴリズムを考えないといけません。


この重量変化に加えてにおいセンサーで精度よく判断したいところですが、部屋自体の臭気など日によって環境が違うことから確実に判断できるとは今のところ言えません。

匂いの反応値と、反応までにかかった時間を書き出してみました。

整髪料の使用時など、部屋の空気清浄機の反応と同じタイミングで発火しているのでセンサーは問題なく機能していますが、臭気センサーのレベル調整が少し面倒でした。

結果として、0.7%程度のセンサー電圧の変化でデータを送信するようにしました。

 

何度か排泄などのタイミングでセンサー値を比べてみると、整髪料を使った時は200以上の値が100計測(50秒)以内に出現しているのに対して、排泄臭はやや穏やかな反応(20程度が100計測以上)を示しているのでこの辺で生活臭との分別は出来そうです。

いずれにせよ、排泄のタイミングと臭気は追って分析していく必用がありそうです。

 

これでサーバー側の処理の必要最低限の処理は一通り出来ましたが、精度を求めるとなるとまだまだ工夫が必要です。

 

次回の第7回目の記事はもう少しデータを蓄積した後に掲載したいと思います。

 

ちなみにですが、製品のトレッタはデータを動物病院と連携させるプログラムが実現されていて自作ではとても不可能な充実したケアが行えるようです。

至れり尽くせりで本当にすごいプログラムなので一見の価値アリです↓

jp.tolettacat.com

猫の体重管理のためにスマートトイレを自作してみた ⑤計量システムの完成

前回、重量センサーの値をサーバーへアップロードすることができたので、今度はにおいセンサーをインストールします。

dreamerdream.hateblo.jp

 

回路は以前の記事の2SC1815を使ったものをそのまま採用

dreamerdream.hateblo.jp

 

構想

においセンサー付きの猫用スマートトイレは見たことがないので世界初なのではないでしょうか?(しらんけど)

実際には今からデータを蓄積して運用しながら解析して使えるかどうか判断することになりますが、においセンサーを付けることにより、

  • 重量変化の分析だけより糞尿の識別が簡単になる(猫のうんこは臭い)
  • 便の臭気を可視化することで体調管理に役立つ

と考えていますが、はたしてうまくいくかどうか。。

 

 

重量センサー、においセンサー、インジケータLEDを搭載した基板がこちら。

ESP32モジュールに元々付いていたピンは撤去して上から線を伸ばして基板へ配置。こうすることで将来ESP32本体の取替が必要になった場合にも線を切るだけで容易に交換することができます。本体も少しだけですが薄くすることができます。

dreamerdream.hateblo.jp

 

 

重量計へ接続したらこんな感じ。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が点滅するようにしました。視覚的にトイレ掃除を促せます。

youtube.com

 

しかし、市販されているCatlogBoardはこのサイズで電池駆動で長期運用可能だというのですから驚きです。作ってみて解る市販品の凄さですね。

 

続き

dreamerdream.hateblo.jp

 

 

猫の体重管理のためにスマートトイレを自作してみた ④計量データをサーバーへ送る

前回はロードセルで重量データを取得することができました。

dreamerdream.hateblo.jp

 

今回はそれをサーバーへ送るわけですが、ロードセルの値は温度変化などで微妙に変わることがあるため常に一定とは限りません。

ドリフトと言って、電源を入れた直後からロードセル自体が熱を持ち、徐々に値が変化する現象もあるようです。トイレ掃除など重量変化の要因はたくさんあります。

「いつ猫が乗ってくるかわからないけどタイミング良く0gに補正して、猫が乗って2000gになって、降りたら10gになったから「猫の体重2000gで排泄10gとする」と計算して結果を送信する。」

なんてコードをコンパイルして書き込み、動作テストをしながら都度修正していくという作業は実に面倒な作業になるのでここではしません。

 

じゃあどうするか?ですが、難しいことはサーバーに送ってから処理すれば良いのです。スクリプト言語のpythonならコンパイル不要で元のデータさえあればいろいろ試すことが出来ます。

 

装置はサーバーへオフセット値無しの単純な計測値のみ送信します。

メモリーの制約や速度をあまり気にしなくて良いサーバー側で過去のデータを遡って乗る前の重量と降りた後の重量を判断させれば、ややこしい計算をESP32でさせなくて良いのです。

加えて生のデータをサーバに蓄積しておけば後で見返すことも過去データの訂正も容易になります。

 

データは常にサーバー送るとリソースの無駄なので、「猫の体重分が装置から減った」という条件をトリガーとして「過去5分程度の生の計測値計」を纏めてサーバーへ送ることにします。

 

過去5分なら約1秒ごと計測で計測値300個分の値を送れば良いので楽勝です。

 

こんなコードを書きました。

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

 

const char *ssid = "【接続先のSSID】";
const char *password = "【接続先のパスワード】";
const char *uploadWeightURL = "【計測結果をPOSTで飛ばす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; //データ送信フラグをスルーするカウント

#define pin_dout  27 //yellow
#define pin_slk   26 //white

//---------------------------------------------------//
// ロードセル シングルポイント( ビーム型) 50kG*4
//---------------------------------------------------//
#define OUT_VOL   0.001f      //定格出力 [V]
#define LOAD      200000.0f    //定格容量 [g]

float offset;

 

/*計測結果の配列を{'W':[12,34,56]}のようなjson形式のStringに変換*/
String getJsonOfWeight() {
  DynamicJsonDocument jsonDocument(5120);  // JSONデータの最大サイズを指定
  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データを指定URLへPOST*/

void sendPostJsonData(String targetURL, String jsonData){
  HTTPClient http;
  http.begin(targetURL);
  http.addHeader("Content-Type", "application/json");

  int httpResponseCode = http.POST(jsonData);
  if (httpResponseCode > 0) {
    String response = http.getString();
    Serial.println("HTTP Response: " + response);
  } else {
    Serial.print("HTTP Error: ");
    Serial.println(httpResponseCode);
  }

  http.end();

  delay(2000);
}

/* 蓄積した計量結果をサーバーへ送る */
void sendWeight(){
  sendPostJsonData( uploadWeightURL ,getJsonOfWeight() );
}

//配列の最後に追加
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;
    }
}


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());

}


void setup() {
  Serial.begin(115200); 

  connectToWiFi();

  Serial.println("AE_HX711 Init");
  AE_HX711_Init();
  AE_HX711_Reset();
  delay(1000);
  offset = AE_HX711_getGram(20);
//  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] > 1500 ){
            //連続してデータを送らないようにカウント中はこのチェックを無視する
            cntNosend = 10;
            return true; 
        }
    }
  
    return false;
}

void loop() {

  int data = (int)AE_HX711_getGram(10);
  addToWeightArray( data ); //オフセット無しの値

  //値の確認用なのでオフセット引く
  String S1 = String(data - (int)offset);
  Serial.print("g:");
  Serial.print(S1); // Print the String to the serial monitor
  Serial.print(" stock:");
  Serial.println( String( currentWeightIndex ) );

  if( checkWeight() ){
    sendWeight();
  }
  
  delay(1);
 
  // WiFi接続が失われた場合に再接続を試みる
  if (WiFi.status() != WL_CONNECTED) {
    connectToWiFi();
  }

}


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( HX711_AVDD);   
  //Serial.println( HX711_ADC1bit);   
  //Serial.println( HX711_SCALE);
  //Serial.println( data);   
  data =  (data / HX711_SCALE )/2; //何故か2倍の値が出てたので1/2とする

  return data;
}

 

サーバー側はDjangoで構築してPOSTで待ち受けます。

dreamerdream.hateblo.jp

DjangoアプリのPOSTには通常csrftokenが付与されて安全性が確率されますが、ESP32からtokenをいちいち取得して再度送信するのは面倒です。

Djangoのviews.pyで先頭に@csrf_exemptをつけるとcsrftokenが不要となるので今回はこちらで無効化することにしました。

@csrf_exempt  #CSRF無効化

def weight_upload(request):

  if request.method == 'POST':

    datas = json.loads(request.body)

    //datasを任意の場所に保存するコードとか任意で

    .......

 

  return HttpResponse("OK")

ひとまずエンドポイントへ接続して保存出来たらOKを返答しています。

youtu.be

これぐらいの容量だとアップロードも一瞬ですね。

 

保存されたデータは

{

    "W": [

        365517,

        365514,

        .

        .

         (300行もあるので略)

        .

        .

        365516,

        365517,

        365517,

        365518,

        365518,

        365517,

        365516,

        365517,

        366960,

        367226,

        367359,

        367323,

        366734,

        365518,

        365519,

        365517,

        365517,

        365519,

        365518

    ]

}

 

このようになります。

この重量データの差分をサーバーで解析することにより、

  • 体重
  • 糞尿量
  • トイレ滞在時間

が分析できるというわけです。

 

ここまででシステムの50%程度の完成です。

 

あとは、送信時のエラー対策や不具合発生時のESP32の自己再起動コードなど盛り込み、サーバー側でスマホ対応のWebアプリを作ります。

先は長い・・・

 

実際に作ってみると市販品は高いといえど、安全面や保証や操作性なども価格に反映されているのでいかにコスパが良いかよくわかります。

自作すること自体に楽しさを見出だせなければこの先のデータの解析なんかは特に苦痛な作業となります。未来の自分に「後は頼んだ!」と丸投げしましょう(笑

 

続き

dreamerdream.hateblo.jp

 

猫の体重管理のためにスマートトイレを自作してみた ③ロードセルの読み取りコード作成

前回はロードセルを組み込んだ重量計本体を作りました。

dreamerdream.hateblo.jp

 

今回はロードセルを利用するためにESP32にコードを書き込みます。

いろいろ便利なライブラリがあるようですが、個人的には中身を見て弄りたいので、こちらのサイトに載っている秋月電子のサンプルコードを拝借させていただきます。

 

<参考サイト>

wscat.cattaka.net

 

<秋月のサイト>

akizukidenshi.com

 

サンプルソースをダウンロードします。

 

Amazonから送られてきたロードセルの詳細情報は不明ですが、秋月電子で販売されているこちらと同様のものだと仮定してデータシートを参考にすることにします。

<秋月で売られているロードセル>

ロードセル 4ポイント(薄型) 200kg(50kgx4): センサ一般 秋月電子通商-電子部品・ネット通販

 

ロードセル情報を書き換え

<HX711のデータシート 秋月電子>

http://akizukidenshi.com/download/ds/akizuki/ae-hx711_rev1_20180129.pdf

基準電圧はHX711内の20Kと8.2Kの分圧抵抗を利用して基準電圧と差異を求めているようです。グラム換算の関数は秋月のサンプルが5V(4.5~5.5V)用なので3.3V用に書き換える必要がありそうです。

よくわからない項目もありますが、こんなかんじ?

 

ひとまず、拝借したコードでそのままグラム換算した値をそのまま出してみました。

void loop() {

  float data;

  data = AE_HX711_getGram(1); //平均化しない
  
  String S1 = String(data - offset, 2);
  Serial.print("g:");
  Serial.println(S1); // Print the String to the serial monitor
  delay(1);

}

 

ノイズを含めたセンサからの値が出力されます。(センサ値そのものではなく、上記秋月のコードでグラム換算しています。)

無事に重量の出力が確認できたら、キッチンスケーラーで100gの錘(ペットボトル含めた水の重量)を作って体重計の上に載せて誤差を確認しました。

中央だけでなく、4隅に置いた場合の値を確認しましたが、どこに置いても同じような値が取得できました。すばらしい!

確認したところ、100gの錘で約200g、200gの場合で約400gを出力しましたので、プログラムで補正1/2としました。(どうしてちょうど倍ぐらいの値が出力されているのかは謎だけど・・・まあいいです)

 

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( num )*HX711_ADC1bit; 
  data =  (data / HX711_SCALE )/2;  //<-ここを1/2に


  return data;
}

 

実験中に猫が乗ってきたのでそのまま体重測定。

1g単位は難しいけど10g単位ならなかなか正確に測れていそうです。

youtu.be

 

アルミホイルでロードセルの線をシールドする前に動作確認した値がこちらなので、もしかしたらシールドが効果を発揮したのかもしれません。

シールド前は20gぐらいたまに大きく変化しています。

 

 

ここまでのロードセル用のコード。

(WiFi接続とかロードセルに不要な諸々のコードを切り取ったものなので、実際にこのままコンパイルするとどこかでエラーが出るかもしれません)

平均化は、ノイズ対策として、計測値の最大値と最小値を無視するという使用にしています。(他のブログで、「突然とんでもない値が出ることがある」と目にしたのですが、実験中に大きく値が飛ぶことはありませんでした。)

#include <Arduino.h>


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);

#define pin_dout  27
#define pin_slk   26

//---------------------------------------------------//
// ロードセル シングルポイント( ビーム型) 50kG*4
//---------------------------------------------------//
#define OUT_VOL   0.001f      //定格出力 [V]
#define LOAD      200000.0f    //定格容量 [g]

float offset;


void setup() {
  Serial.begin(115200); 

  Serial.println("AE_HX711 Init");
  AE_HX711_Init();
  AE_HX711_Reset();
  delay(1000);
  offset = AE_HX711_getGram(30);
  Serial.println("offset : ");
  Serial.println( offset );
}

void loop() {

  float data;

  data = AE_HX711_getGram(10);
  
  String S1 = String(data - offset, 2); // Convert float to String with 3 decimal places
  Serial.print("g:");
  Serial.println(S1); // Print the String to the serial monitor
  
  delay(1);

}


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( HX711_AVDD);   
  //Serial.println( HX711_ADC1bit);   
  //Serial.println( HX711_SCALE);
  //Serial.println( data);   
  data =  (data / HX711_SCALE )/2;


  return data;
}

 

あとは猫が乗って降りたときの値を蓄積してサーバーへ飛ばしてやれば体重計はほぼ完成ですね。

 

後で思いついたのですが、重量計の上にキッチンスケーラーをおいてオフセット値としておけば、スケーラーの値との差分をリアルタイムで見比べることができます。

 

続き

dreamerdream.hateblo.jp

 

 

猫の体重管理のためにスマートトイレを自作してみた ②体重計本体の作成

前回、ロードセルの使い方が解ったので今回は購入したロードセルを使った体重計本体を作ります。

dreamerdream.hateblo.jp

 

 

Catlog Baordのような形が理想ですが、形は手元にある端材で作りながら考えることにします。

 

トイレの下に丁度良さそうなサイズの板を発見しました。ラッキー☆

 

トイレの下面を新聞紙にトレースし、ロードセルも含めてケージに収まるか確認します。

 

トレースした下面のサイズと手元にある板のサイズを比較しながらいろいろ構想を練ります。

トイレが体重計からズレ落ちないようにしたいです。

 

3Dプリンターで木材切り出し用の治具を作りました。

3Dプリンターのサイズが足りないので長いものは3Dペンで結合。

 

治具を固定しながらトリマーで必要パーツを切り抜きます。

 

トイレ固定用のパーツ完成。

 

ロードセル設置用の穴開け中。

このベアリング付きビットはベアリングがガイドに沿うことで形通りに削れるのでめちゃ便利です。

トリマーは1つあればかなり木工の幅が広がるのでここでオススメしておきます。

 

ロードセル設置穴開け完了。

 

ガイドが途中でズレて凹んでしまった部分は後で修正することにします。

中ブロックのロードセルに干渉する部分も削っておきます。

 

ボンドで貼り付けて配線用の溝も掘りました。

 

周囲はしっかりとヤスリで削って形成します。

 

凹んでいた部分も修正。(パテを切らしていたので紙粘土で)

 

最終的にトイレの下に敷くので、汚物が付くと嫌だし水拭きもしたいので、防汚対策として分厚く塗装をしたいところですが、下手に塗装をすると猫が塗料の匂いを嫌がる可能性があるのでカッティングシートを貼り付けることにしました。

 

カッティングシートはドライヤーで温めると結構伸びるので意外と曲面でも貼り付けられます。車のデコレーションでよく使われるラッピングシートとの違いはよく知りませんが似たようなものでしょうか?

 

貼り付けて完成したものがこちら。トリマーで削った縁の曲面が滑らかなのでぱっと見は元がベニヤ板だとは気づかれないでしょう。

すこし雑な部分もありますが下手に塗装をするより確実な防汚対策になります。

触った感じは油性塗料を濃く塗ったかのような仕上がりです。

 

裏面にロードセルを設置して仮配線して計量テストをします。

ロードセルの線が細いので実験中に切らないようテープでしっかりめに止めています。

 

この仮止め状態でテストしたところ、負荷がかかるとマイナス値を出したので配線のA+とA-を入れ替えました。(プログラムの方で弄っても良い)

他のブログでも初期配線でマイナス値を出したという例が見受けられましたので、回路の固定前に実際に回路を作って動作テストをするのが良いと思います。

 

注意点として、ロードセルの抵抗値変化は微量なので、軽微なノイズでもアンプが誤検知をしてしまいます。

そしてロードセルから出ている線も実はそれ含めてロードセルの抵抗の一部なので極力切らないようにしないといけません。

ただ、余った線をいい加減にまとめてコイルを形成してしまうと外部からノイズの影響をもろに受ける可能性もあります。

ですのでアルミホイルを巻いた上にドレイン線としてその辺の導線を巻いて簡易的なシールドを作りました。

巻き方は適当ですが何も対策をしないよりはノイズに強くなることでしょう。

(このアルミホイルを巻くというシールド作りが地味に面倒な作業で、空いた時間にちょこちょことして何日も余分にかかりました。・・・がんばった)

 

シールド用アルミホイルの切り方↓

dreamerdream.hateblo.jp

 

シールドはドレイン線を介してアンプのGNDへ繋げています。

足は3Dプリンタで作って、本体を浮かせるために2mmほどのゴム膜を貼りつけました。

その上から防汚対策としてカッティングシートを貼って完成。

万が一汚れても除菌シートなどで簡単に拭き取れます。

基板はケージに取り付ける予定です。

体重計の最大高は約21mm。(参考にしたCatlogBoardの高さは37.7mm。)

 

実際にトイレが高くなるのは12mmだけなので猫も全く違和感なくトイレに入ってくれることでしょう。


主要部品が完成したので今度はアンプへ繋げて重量計測のコードを書いていきます。

dreamerdream.hateblo.jp

 

猫の体重管理のためにスマートトイレを自作してみた ①ロードセルセンサーの構造と回路を知る

猫用スマートトイレの「トレッタ」をご存知でしょうか?

猫がトイレで用を足すごとに、

・体重・尿量・トイレ回数・トイレ滞在時間

などがスマホで簡単に記録でき、猫ちゃんの健康管理に大いに役立つという商品です。

猫は腎臓の病気を患うことが多いのでトイレの管理ができるとそれだけ病気に早く気付けるというわけですね。

そして、このトレッタのすごいのが特許の「猫専用顔認証システム!!!」これにより他頭飼いの場合でも各猫ごとの記録ができちゃう!というのです。す・すごい!


カメラなしの純粋にトイレの重さだけ量るCatlog Boardというものもあるんですね。

ただ、トレッタは1台毎に、CatlogBoardは1匹ごとに月額費用が発生する。。。「自作なら月額0円だし、自作しちゃおう!」というのが今回の工作の浅はかな動機です。

もし今後作ってみようかなという方の参考になれば幸いです。

 

我が家は猫は1頭だけなので顔認証システムは必要なく、体重や尿量の管理をするトイレ型体重計を作る。というのが第一目標です。

 

※トレッタやCatlog Boardなどは体重や尿量や健康状態などをアプリで一元管理できて体重や尿量の増減によって注意事項や獣医師からのアドバイスが表示されたりと、誰にでも見やすく丁寧な作りになっていますので、今回作ろうとしている素人作品とは全然違うものですよー。ということは先にお伝えしておきます。

 

という前置きをし、Amazonで体重計測に使えそうな部品探しと情報収集をしました。

 

重量のセンサーには「ひずみゲージ」と呼ばれるセンサーを金属ブロックと一体化させた「ロードセル」というセンサーが一般的に広く使われているそうです。

一般家庭にある体重計ももちろんロードセルが採用されています。

今回は初めて使うこのロードセルセルセンサーの構造と使い方の情報収集をして纏めます。

 

ロードセルの候補

Amazonで安く手に入るのは、50Kgのロードセルが4個とアンプ(後述)がセットとになったこちら↓

ハーフブリッジ(後述)というロードセルが各ブロックに搭載されていて、このブロックは1つ、2つ、4つを組み合わせて使えるようになっています。

ロードセルは元来割と高いものらしく、1つ500円以上もザラ。体重計をわざわざ分解して部品取りをしたほうが安いという時代もあったようです。

秋月ではこのセル4つセットだけで2000円弱するので本当に破格ですね。

akizukidenshi.com

1つ50Kgの耐荷重なので4つ使うと200Kgまで計量することができます。

しかし、これはトイレ含めて10kg未満の今回の用途にはオーバースペックのように見えます。ブロックが硬すぎて歪みにくいと求めたい精度が出ないんじゃないかな?と感じます。

 

50Kgもいらんし、、と他を探してもあまりしっくりくるものが無いんですよね。。。

こちら5Kgのハーフブリッジロードセルが4個セットになったもの↓

合計20Kgだと今回の用途には良さそうです。しかし先程のものを見てしまった後だとちょっと割高に感じるのと、アンプが付属していないので別途購入する必要があります。悩みどころです。

 

こちらは1Kgのフルブリッジ(後述)のロードセルが4個とアンプも4個セットになったお買い得な商品↓

合計で4Kgまで計量することができます。

トイレ+猫となると明らかに強度不足・・・

1つで10Kg対応の商品ならあるんだけどトイレのサイズを一点で支えるとか絶対に素人工作では無理なので4隅にセンサーは置きたいんですよね。

 

なかなか悩ましい選択肢です。

ロードセル自体は金属板にひずみゲージを取り付けた単純なものなので、一見自作も可能に見えます。

ひずみゲージ単品も入手可能です。

しかし、ひずみゲージの取り付け方や製作工程などを調べるとそれぞれの形状に理由があったり、ひずみゲージを取り付けるのに推奨される接着剤があったりと、正確な重量を計るロードセル自体を素人が自作するのはなかなか難儀なように見えます。

www.aandd.co.jp

対象物が「乗った」「降りた」程度の検知であれば自作でも可能だと思うのですが、ロードセルの研究から始める気もしませんし、体重管理が目的となると既製品に頼るのが無難です。

 

アンプについて

最近の電子工作ではロードセルのセンサー値の演算機として「HX711」というA/D変換器をセットとして使うようで、アンプと呼ばれているそうです。

こちらのブログはこのアンプの登場以前のことを知る方が書かれたもので、計量するならHX711がいかに便利で使わない手がないICだということが書かれています。

a-tomi.hatenablog.com

ノイズに関するることや信号線の扱い、ブロックの歪み方や注意点など細かく書かれているので、ロードセルセンサーを使うなら一読する価値ありのブログです。

 

ロードセルのハーフブリッジとフルブリッジの違いについて

商品紹介でハーフブリッジフルブリッジのロードセルを紹介していました。これはブロックに設置されているひずみゲージの数と、そこから出ている線の違いです。

フルブリッジ↓

ひずみゲージ(抵抗体)が4つ採用されいるロードセルを使う上で基本的な回路です。抵抗体それぞれから線が出ています。

赤に+、黒にーを接続し、緑と灰色の抵抗値の差を読み取ることで変化値を算出します。

この回路はホイートストンブリッジと呼ばれる回路で、繊細な抵抗の変化を精度よく読み取れる回路だそうです。

<詳細はWikipedia参照>

ja.wikipedia.org

 

 

ひずみゲージを2つだけ搭載したものがハーフブリッジ↓

よくある抵抗分圧の回路ですね。

これを2つ繋げばフルブリッジとなりますね。

そして先述のアンプへ入力することで抵抗値変化をより解りやすく大きく捉えられるようになります。

 

ハーフブリッジのロードセルを1つ、2つ、4つでそれぞれ繋ぐ場合の回路がこちらに掲載されています。

50kg Load Cells with HX711 and Arduino. 4x, 2x, 1x Diagrams. - Circuit Journal

 

ハーフブリッジ4つでフルブリッジ??

参考先で繋ぎ方はわかるものの、4つの場合は「これでフルブリッジ??なんで??」と思いますよね。

ハーフブリッジを2つで抵抗体4つのフルブリッジになることは理解に容易いと思いますが、抵抗体8つですよ?不思議だと思いませんか?

何なら「フルブリッジのロードセルも配線の仕方ではハーフブリッジとして使えるんじゃないの?」とも思いますよね?

 

配線がややこしいので一見複雑に見えますが、文字通りこの入り組んだ配線を紐解くと理解しやすいと思いますので少し説明します。

ハーフブリッジの場合、要するに内部は単なる抵抗2つの分圧回路です。

この抵抗から出ている3本の線を各々の抵抗へ接続してフルブリッジ回路を形成するのですが、このままでは解りにくいので、

 

この配線を一度立体的に図示してみます。

だんだん見えてきましたでしょうか?

展開すると、なんと抵抗体2つ直列のフルブリッジが完成するってワケなんですね。

このように4つのロードセルを結合することで1つのホイートストンブリッジ回路が形成され、正確な抵抗値が算出できるというわけです。

 

なので、元々フルブリッジのロードセル4つを1つ線を無視して同じように配線したとしても余計な抵抗が内部に形成されているので正確な抵抗値が算出されるようにはなりません。残念!

もし、フルブリッジを4つ配置したい場合はロードセルを単純に並列に繋ぐようです。

<参考>

ロードセル

製品に関するFAQ | オムロン制御機器

 

並列に繋いだものを表すとこうなります。


ロードセルを並列に繋ぐということは、抵抗の並列接続ですので消費電力が数倍増えることになるのでアンプが消費電力に対応しているかの確認をする必要があります。

 

また、それぞれにアンプを設けてマイコンで計算させるという方法もあります。

こちらのブログでは4つのフルブリッジロードセルをクロック線を同一にして利用するという方法が掲載されています。

asukiaaa.blogspot.com

 

作成例

ハーフブリッジのロードセンサーを実際に使った方の参考になるブログ紹介です。

kohacraft.com

 

そして今回、僕が作ろうとしているものを実際に作られている方々のブログも発見!!

結構いらっしゃることに驚きです。

wscat.cattaka.net

fabcross.jp

www.gtworks.dev

 

これらのブログを拝見させていただきますと、50Kgのロードセル4つでも割と正確に重量検出ができていそうです。

ということで、ものは試しにこちらを購入してみました↓

ロードセル8個、アンプ2個、リボンケーブルまでおまけに入って1000円以下の大変お買い得な商品です。

これなら仮に失敗しても「あちゃー」で済みますしね(笑

※実際に届いたものがこちら↓

dreamerdream.hateblo.jp

 

もしこれで精度が悪すぎたら5Kg×4個のロードセルを単体購入することにします。

 

ロードセルを利用して猫の排泄時の挙動から糞尿の判断や算出をする方法はこちらのブログにきれいに纏められており、参考になります。

www.gtworks.dev

大と小との判断をロードセルの値から賄うのはやや面倒そうです。

 

スマートトイレだけでなく、猫と電子工作の相性は抜群のようで、こんな本まで出ています。(笑

スマートねこじゃらしとか出来たらおもしろそう。

 

続き

dreamerdream.hateblo.jp

 

ESP32でWiFi接続したらアナログ入力ができなくなるという罠にかかった 備忘録

前回、ESP32でにおいセンサーを利用することが出来たので、意気揚々と回路を作成してWiFi接続して取得値をサーバーに送ろうとしましたところ、、、

dreamerdream.hateblo.jp

センサー値

0,0,0,0,0,0,0,0,0,0

 

「え?え?え?え???なんで????

もしかして半田ごての熱でセンサー壊れた?」

試しにテストコードを書き込んだらきちんと値が出る。

 

コードの問題だということで、いろいろ試した結果、「WiFi接続を試みると取得値が0になる。」という事実が判明しました。

 

回路組んだ後でしたので結構ショックです。

調べるとこの記事を発見。

rikoubou.hatenablog.com

 

GPIOの2,4,12,14,15,26,27はWiFi.beginを実行したら使えなくなるというのです。

これも環境によるのかもしれません。試しに21,22,23などを試したところ、それも駄目。(もともとアナログ入力ポートでは無かったのに気が付かず…)

「え?待って!これ使えるA/D変換ピン無いんじゃないの?」

とこちらの、デフォルトで入力と無設定になっているピンをすべて試してみました。

lang-ship.com

 

コードはこのように

 

結果・・・・

32と33が辛うじて使えるようです。

32は値が安定しないのですが、33はプルダウンされている様子で、ピンに触れないと値は変わりませんでした。

他のピンはすべて駄目!?(←アナログ入力ピンでないことに気付かないまま)

 

結果的に33をニオイセンサー用に使うようにしました。

 

過去にESP32を使ったコントローラーを作った時にはもっとたくさんアナログ入力ができた気がするのですが、たまたま使えるピンを採用していたようで…

dreamerdream.hateblo.jp

 

どうやらWiFi使用中はADC1(IO32~IO36,IO39)の6ポートだけしか使用できないという情報を得ました。

これはかなり落とし穴でした。

kampa.me