洗濯物乾燥監視装置(製作編)

2025年の梅雨の時期は短く、その時期は過ぎてしまったが、洗濯物を室内干しする機会が増えるものと思い、屋外に干した場合、屋内に干した場合、サーキュレーターを使った場合、乾くのが早そうなハンガーを使った場合などで、乾くまでの時間がどの程度違うのかを確認するため、ハンガーにかけた状態で濡れた洗濯物の重量を一定時間ごとに測定し、乾くまでの重量変化を記録できる洗濯物乾燥監視装置を作ることにした。

ハードウェア

コントローラだが、重量センサ(計量ユニット)、温湿度センサ、LCD、マイクロSDカード、リアルタイムクロックなどが使いやすい機種にしたいと考えた。下記の記事では、個々の周辺モジュールを電線で接続して構成したが、今回は、組み立て作業が楽で、小型化が図れるM5Stack Core2 V1.1を使うことにした。M5Stackには複数の種類があるが、リアルタイムクロックをバックアップする電源も内蔵しているとのことだったので、その機種を選んだ。

M5Stack Core2 V1.1

物干し竿にその装置をぶら下げ、その装置に、ハンガーにかけた被測定物をぶら下げる構造とし、測定終了まで内蔵電池で動作させたかったので、M5Stack Core2 V1.1に取り付けるM5GO Bottom2(M5Stack Core2用バッテリーボトム)でバッテリー容量を増やそうとした。しかし、M5GO Bottom2はバッテリー容量を増やすものではなく、もともとM5Stack Core2 V1.1に内蔵されている500mAhのバッテリーを、M5GO Bottom2に内蔵されている500mAhのバッテリーに置き換えるものであることに現物を見るまで気付かなかった。バッテリー容量に関しては、モバイルバッテリーを接続することにした。複数のAIに、M5GO Bottom2はバッテリー容量を増やすものかという質問をしたが、どのAIもバッテリー容量を増やすものという、間違った回答をした。それだけ、有効な情報源が無かったということかもしれない。
M5GO Bottom2が全く無駄になってしまったというわけではなく、M5GO Bottom2の底面部には「STICK HOLE」というLEGO互換穴があり、購入した計量ユニット(MiniScales(HX711 STM) 5kgレンジ)や、温湿度センサのUNITにもその穴があり、購入し放置していた「M5GO IoT Starter Kit V1.0」(販売終了している模様)にLEGOのパーツが入っていたので、そのパーツと組み合わせることができた。また、「M5GO IoT Starter Kit V1.0」には、I2C仕様の温湿度センサ(ENV SENSOR)や、M5Stack Core2に複数のI2C仕様のUNITを接続するためのGROVE HUBも含まれていたのでそれらを使うことにした。

M5GO Bottom2

M5Stack core2 V1.1の底部ユニット
これを外して、M5GO Bottom2を取り付ける

M5GO IoT Starter Kit V1.0(2018年頃の商品らしい)

これまでに説明した構成を組み上げた装置本体を下図に示す。数が限られたLEGOのブロックを使い、配線ができるか等を確認しながら各UNITの配置を決めたが、配線すると途端に見栄えが悪くなった。

装置本体表側
(M5Stack Core2 V1.1の底部ユニットをM5GO Bottom2に交換した状態、筐体等を除く)

装置本体裏側

動作設計

とりあえず、校正と、重量のリアルタイム表示ができるように、ChatGPT 4o等に頼んでコード(スケッチ)を作成してもらった。具体的には、計量ユニットに何も乗せていない状態でAボタン(一番左の静電容量スイッチ)にタッチするとゼロ点校正(オフセット取得)が行われ、500gの分銅を計量ユニットに乗せてBボタン(真ん中の静電容量スイッチ)にタッチすると500g校正(スケーリング係数取得)が行われるようにしている。また、校正によって求められた校正値はマイクロSDカードに記録し、電源投入時、それらの値を読み出すように構成している。

最終的には、濡れていない被測定物の重量を予め測定し、その重量を目標重量として、被測定物の重量変化を記録するように構成する。測定開始すると、測定開始時刻を記録した後、経過時間、重量、温度及び湿度を一定時間毎にマイクロSDカードに記録し、目標重量に近づいた(達した)と判断された時の経過時間を記録する。
現状のコード(スケッチ)を実行していると、表示される重量が変化しているので、被測定物が乾いても、測定値がばらついたり、ドリフトしていく可能性があり、期待する精度が得られない可能性もある。目標重量を含む所定範囲内に、一定時間、測定値が入れば、直ちに、目標重量に達したと判断するというような判定条件にする必要があるかもしれない。また、洗濯すると重量が変わるというようなことがあるかもしれない。これらの点は、実際に試し、AIにも相談しながら対応策を検討していく。
装置本体を、LEGOのパーツの穴に通したネジによって平板に固定し、それを物干し竿から吊るし、計量ユニットの天板Top Plateにワイヤをかけて下方に引っ張る構成を付加する予定。

ソフトウェア

計量ユニット(MiniScales(HX711 STM) 5kgレンジ)に関連するGitHubからサンプルスケッチやライブラリを入手し、それらをChatGPT 4o等に渡して、Arduino IDEでコンパイルするためのスケッチ(校正機能とリアルタイム重量表示機能のみ実装)を作ってもらった。
計量ユニットはI2C仕様でありアドレスは26だった。また、M5Stack Core2 V1.1のI2C仕様のポート(PORT A)のSDAは32、 SCLは33であった。SDAやSCLの番号を調べるのに時間がかかった。M5Stack Core2 V1.1の裏面にその番号が表示されていたが、シニアには小さ過ぎた。

I2C仕様であるPORT AのSDAやSCLの番号が表示されている

現状のスケッチを下記に示す。ただし、作成途中のものであり、また、動作を保証するものではないため参考情報とする。

#include <M5Unified.h>
#include "UNIT_SCALES.h"
#include <SD.h>

UNIT_SCALES scales;

float zero_offset = 0.0;
float scale_factor = 1.0;
const char *CAL_FILE = "/scale_cal.txt";

// SDに校正値を保存(上書き)
void saveCalibration(float offset, float factor) {
    // 既存ファイルを削除してから書き込み
    SD.remove(CAL_FILE);

    File file = SD.open(CAL_FILE, FILE_WRITE);
    if (file) {
        file.printf("%.4f,%.4f\n", offset, factor);
        file.close();
        M5.Lcd.println("Cal saved to SD");
    } else {
        M5.Lcd.println("SD write error");
    }
}

// SDから校正値を読み出し
bool loadCalibration(float &offset, float &factor) {
    File file = SD.open(CAL_FILE, FILE_READ);
    if (file) {
        String line = file.readStringUntil('\n');
        file.close();

        int commaIndex = line.indexOf(',');
        if (commaIndex > 0) {
            offset = line.substring(0, commaIndex).toFloat();
            factor = line.substring(commaIndex + 1).toFloat();
            return true;
        }
    }
    return false;
}

void setup() {
    Serial.begin(115200);
    auto cfg = M5.config();
    M5.begin(cfg);

    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.setTextColor(GREEN, BLACK);
    M5.Lcd.println("Initializing...");

    // SD初期化(M5Stack Core2はGPIO4)
    if (!SD.begin(GPIO_NUM_4)) {
        M5.Lcd.println("SD init failed");
    } else {
        M5.Lcd.println("SD OK");
    }

    // 計量ユニット(重量センサ)初期化
    while (!scales.begin(&Wire, 32, 33, DEVICE_DEFAULT_ADDR)) {
        Serial.println("scales connect error");
        M5.Lcd.println("Sensor Error...");
        delay(1000);
    }
    scales.setLEDColor(0x001000);  // 計測ユニットのLEDを緑色に点灯させる

    // SDから校正値読み出し
    if (loadCalibration(zero_offset, scale_factor)) {
        M5.Lcd.println("Cal loaded from SD");
    } else {
        M5.Lcd.println("Cal not found");
        zero_offset = scales.getRawADC();  // 念のため初期値取得
        scale_factor = 1.0;
    }
}

void loop() {
    M5.update();

    float raw = scales.getRawADC();
    float weight = (raw - zero_offset) * scale_factor;

    // 重さ表示エリアをクリア
    M5.Lcd.fillRect(0, 50, 240, 30, BLACK);
    M5.Lcd.setCursor(0, 50);
    // 浮動小数点を小数点以下2桁で確実に表示
    M5.Lcd.print("Weight: ");
    M5.Lcd.print(weight, 2);
    M5.Lcd.println(" g");

    // シリアルにも出力
    Serial.printf("Weight: %.2f g\n", weight);

    // ボタンA:ゼロ点校正
    if (M5.BtnA.wasHold()) {
        M5.Lcd.setCursor(0, 100);
        M5.Lcd.println("Zero Cal...");
        delay(1000);
        zero_offset = scales.getRawADC();
        scale_factor = 1.0;  // 一旦リセット
        M5.Lcd.println("Zero Done");
        saveCalibration(zero_offset, scale_factor);
    }

    // ボタンB:500g校正
    if (M5.BtnB.wasHold()) {
        M5.Lcd.setCursor(0, 130);
        M5.Lcd.println("500g Cal...");
        delay(1000);
        float raw_now = scales.getRawADC();
        float diff = raw_now - zero_offset;
        if (diff != 0) {
            scale_factor = 500.0 / diff;
            M5.Lcd.println("500g Done");
            saveCalibration(zero_offset, scale_factor);
        } else {
            M5.Lcd.println("Cal error (diff=0)");
        }
    }

    delay(500);
}

今回、M5Stackや計量ユニットのライブラリはどれを使えばいいかで、長時間、悩まされた。M5Stack関連では、M5Stack.h、M5Core2.h、M5Unified.hなどがあり、計量ユニット関連では、UnitMiniScales.h、UNIT_SCALES.h
M5Unit-WEIGHT.hなど。AIがコードを作成してくれるのは楽だが、どのライブラリを使えばいいかというような、横断的で、まとまった知識がネットでも不足しているように思われ、現時点では、これらの面倒な確認作業(知識習得作業)は人が行う必要があり、それによって得られた正しい知識をAIに供給するのが人の役割のようだ。

目次