DreamerDreamのブログ

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

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

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

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

 

kampa.me