【GW自由研究】Arduinoで簡易洗濯機を作りたい

# 背景
GWだけどステイホームで外出できないため，家でできる電子工作にチャレンジした．
コロナ対策としてガーゼマスク[^1]を妻が使っているが，手洗いする必要があるため，その手間を省こうと，**簡易洗濯機**を作ろうと思い立った．
この記事はその~~挫折~~記録である．
[^1]: 妻が使っているマスクは[こんな感じの](https://www.creema.jp/item/9356369/detail)レースマスクである．配られた某マスクではない．

# 先行研究
子供用おもちゃとしてミニチュア洗濯機が販売されている．[こちら](https://www.amazon.co.jp/%E3%82%AF%E3%83%A9%E3%82%A4%E3%83%B3-Klein-Miele-%E3%83%89%E3%83%A9%E3%83%A0%E5%9E%8B%E6%B4%97%E6%BF%AF%E6%A9%9F-6941/dp/B00UCK33HU/ref=asc_df_B00UCK33HU/?tag=jpgo-22&linkCode=df0&hvadid=280429919010&hvpos=&hvnetw=g&hvrand=9456328911747153453&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009343&hvtargid=pla-544440725212&psc=1&th=1&psc=1)のように水を入れて使用できないタイプが多いが，[某100円ショップで販売されているミニ洗濯機は実際に洗濯ができる](https://iemone.jp/article/zakka/mayumi_w_106873/)と話題になった．100円ショップで買えるとあって非常に安価だが，速度調整やきちんとした脱水ができないなど，通常の洗濯機の機能とは乖離がある．~~（あと近くの店で売ってない．）~~
[家電にも小型洗濯機というジャンルがある．](https://sakidori.co/article/80397)勿論機能面は申し分がない．しかし，値段が数千円台とそこそこするのと，**GW暇だから電子工作をしたい**という本来の目的にそぐわない．
[手動洗濯機](https://www.amazon.co.jp/%E3%82%B1%E3%83%BC%E3%82%BA%E3%82%A6%E3%82%A7%E3%83%BC%E3%83%96-Ks-Wave-%E9%9B%BB%E6%BA%90%E4%B8%8D%E8%A6%81%EF%BC%81%E6%89%8B%E5%8B%95%E5%BC%8F%E3%83%91%E3%83%AF%E3%83%95%E3%83%AB%E6%B4%97%E6%BF%AF%E6%A9%9F%E3%80%90MyWave-%E3%83%8F%E3%83%B3%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A9%E3%83%83%E3%82%B7%E3%83%A5%E3%80%91%E5%88%86%E3%81%91%E6%B4%97%E3%81%84%E3%82%84%E9%87%8E%E8%8F%9C%E3%81%AE%E6%B0%B4%E5%88%87%E3%82%8A%E5%99%A8%E3%80%81%E3%82%B5%E3%83%A9%E3%83%80%E3%82%B9%E3%83%94%E3%83%8A%E3%83%BC%E3%81%AB%E3%82%82%EF%BC%81/dp/B07B4YYSG4/ref=zg_bs_5248692051_22?_encoding=UTF8&psc=1&refRID=ZDZAHP8PWWABGMR3S9TS)も販売されている．要するに小さい手動洗濯機を自動で回す制御部分を作れば目的を達成できる．が，マスクを洗うという目的には少しサイズが大きい．

# アプローチ
大きく3つのステップで作成することを検討．

- step1 制御部作成

- step2 洗濯槽作成

- step3 合体

本記事では，step1の制御部作成について記載する．
今回は，大学生の頃に購入し眠っていた**Arduino Leonardo**を活用することにした．
はんだごてすら持っていないので，ジャンプワイヤやブレッドボードでできる範囲で実装した．

## 制御部の要件
- 洗濯物が絡まるのを防ぐため，回転方向を左右交互に繰り返すことができる．

- 洗濯時間を手動で設定できる．

- 残り時間などを簡単に把握できる．

これらの要件を満たすため，[L298Nモータードライバ](https://www.amazon.co.jp/VKLSVAN-%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%89%E3%83%A9%E3%82%A4%E3%83%90-%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%9C%E3%83%BC%E3%83%89-%E3%83%87%E3%83%A5%E3%82%A2%E3%83%ABH%E3%83%96%E3%83%AA%E3%83%83%E3%82%B8-Arduino%E3%81%A8%E4%BA%92%E6%8F%9B/dp/B08B87WWHV)を用いてモータの回転速度と方向を変化させ，タクトスイッチを用いて洗濯時間を変化できるようにし，[OLEDディスプレイ](https://www.amazon.co.jp/gp/product/B01M8JV310/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1)を用いて残り時間や現在の状態を表示し，洗濯終了の通知として，ブザーを用いて音を鳴らすことを検討した．また，モータの回転軸の変換および減速のため，[ユニバーサルギアボックス](https://www.tamiya.com/japan/products/70103/index.html)を用いた．

## 回路図
試行錯誤の結果，図のような回路を構成した．
![回路図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94404/2f01acee-27d0-9933-d4d1-86c98dd94a59.png)
LEDは状態遷移を分かりやすくするために配置した．
回路図はFritzingを使いたかったが有料だったので，[draw.io](https://app.diagrams.net/)で一から描いた．ダイオード・抵抗・ブザーなどの記号はあったので助かった．

## プログラム

```cpp
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Buzzer.h>

#define OLED_RESET 4
#define RED_LED_PIN 13
#define GREEN_LED_PIN 12
#define MACHINE_STATE_BUTTON_PIN 10
#define START_BUTTON_PIN 8
#define DEFAULT_TIME 5
#define MOTOR_ENA_PIN 5
#define MOTOR_INPUT1_PIN 6
#define MOTOR_INPUT2_PIN 7
#define BUZZER_PIN 11

Adafruit_SSD1306 display(OLED_RESET);
Buzzer buzzer(BUZZER_PIN);
int machine_state = 0; // 0: init, 1: washing and drying, 2: finished
boolean switch_on = false;
int switch_count = 0;
int washing_time = DEFAULT_TIME;

void setup() {
	// LED
	pinMode(RED_LED_PIN, OUTPUT); // wash, red
	pinMode(GREEN_LED_PIN, OUTPUT); // dry, green

	// input pin
	pinMode(MACHINE_STATE_BUTTON_PIN, INPUT);
	pinMode(START_BUTTON_PIN, INPUT);

	// motor
	pinMode(MOTOR_ENA_PIN, OUTPUT);
	pinMode(MOTOR_INPUT1_PIN, OUTPUT);
	pinMode(MOTOR_INPUT2_PIN, OUTPUT);

	display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3D (for the 128x64)
	display.clearDisplay();
	display.setTextSize(2);
	display.setTextColor(WHITE);
	display.setCursor(0,0);
	display.println("Hello");
	display.println("(^_^)");
	display.display();
	delay(1000);
	display.clearDisplay();
	Serial.begin(9600);
}


void rotation(int start_v, int peak_v, int step_v, int peak_time, int stop_time, int step_time){
	digitalWrite(MOTOR_INPUT1_PIN, HIGH);
	digitalWrite(MOTOR_INPUT2_PIN, LOW);
	for (int i = start_v; i <= peak_v; i+=step_v) {
		analogWrite(MOTOR_ENA_PIN, i);
		delay(step_time);
	}
	delay(peak_time);
	for (int i = peak_v; i >= start_v; i-=step_v) {
		analogWrite(MOTOR_ENA_PIN, i);
		delay(step_time);
	}
	digitalWrite(MOTOR_INPUT1_PIN, LOW);
	digitalWrite(MOTOR_INPUT2_PIN, LOW);
	delay(stop_time);
	digitalWrite(MOTOR_INPUT1_PIN, LOW);
	digitalWrite(MOTOR_INPUT2_PIN, HIGH);
	for (int i = start_v; i <= peak_v; i+=step_v) {
		analogWrite(MOTOR_ENA_PIN, i);
		delay(step_time);
	}
	delay(peak_time);
	for (int i = peak_v; i >= start_v; i-=step_v) {
		analogWrite(MOTOR_ENA_PIN, i);
		delay(step_time);
	}
	digitalWrite(MOTOR_INPUT1_PIN, LOW);
	digitalWrite(MOTOR_INPUT2_PIN, LOW);
	delay(stop_time);
}

void wash(){
	// ( (peak_v - start_v) / step_v + 1 ) * step_time * 4 + peak_time * 2 + stp_time * 2
	// = 20 sec
	// --> * 3 --> 1 min
	for (int k = 0; k < 3; k++){
		rotation(80, 150, 10, 4800, 2000, 200);
	}
}

void washing(int washing_step, int drying_step){
	char buf[64];
	digitalWrite(RED_LED_PIN, HIGH);
	for(int i = washing_step; i > 0; i--){
		display.clearDisplay();
		display.setCursor(0,0);
		sprintf(buf, "Washing %d", i);
		display.println(buf);
		sprintf(buf, "Drying  %d", drying_step);
		display.println(buf);
		display.display();
		wash();
	}
	digitalWrite(RED_LED_PIN, LOW);
}

void dry(){
	// ( (peak_v - start_v) / step_v + 1 ) * step_time * 4 + peak_time * 2 + stp_time * 2
	// = 20 sec
	// --> * 3 --> 1 min
	for (int k = 0; k < 3; k++){
		rotation(80, 250, 10, 4400, 2000, 100);
	}
}

void drying(int washing_step, int drying_step){
	char buf[64];
	digitalWrite(GREEN_LED_PIN, HIGH);
	for(int i = drying_step; i > 0; i--){
		display.clearDisplay();
		display.setCursor(0,0);
		sprintf(buf, "Washing %d", washing_step);
		display.println(buf);
		sprintf(buf, "Drying  %d", i);
		display.println(buf);
		display.display();
		dry();
	}
	digitalWrite(GREEN_LED_PIN, LOW);
	display.clearDisplay();
	display.setCursor(0,0);
	display.println("Washing 0");
	display.println("Drying  0");
	display.display();
	delay(1000);
}

void play_music(){
	buzzer.begin(10);
	buzzer.sound(NOTE_C3, 600);
	buzzer.sound(NOTE_D3, 300);
	buzzer.sound(NOTE_E3, 300);
	buzzer.sound(NOTE_F3, 300);
	buzzer.sound(NOTE_G3, 300);
	buzzer.sound(NOTE_A3, 300);
	buzzer.sound(NOTE_B3, 300);
	buzzer.sound(NOTE_C4, 600);
	buzzer.sound(NOTE_B3, 300);
	buzzer.sound(NOTE_A3, 300);
	buzzer.sound(NOTE_G3, 300);
	buzzer.sound(NOTE_F3, 300);
	buzzer.sound(NOTE_E3, 300);
	buzzer.sound(NOTE_D3, 300);
	buzzer.sound(NOTE_C3, 600);
	buzzer.end(2000);
}

void loop() {
	Serial.print("machine state");
	Serial.println(machine_state);
	Serial.print("slide switch");
	Serial.println(digitalRead(START_BUTTON_PIN));
	boolean t = digitalRead(START_BUTTON_PIN);
	// machine_state = switch_count % 3;
	if (machine_state == 0) {
		boolean now_switch_state = digitalRead(MACHINE_STATE_BUTTON_PIN);
		if (now_switch_state && !switch_on) {
			switch_count++;
		}
		delay(50); // チャタリング防止用．本当はハードでも対処した方が良いが今回はスキップ
		switch_on = now_switch_state;
		washing_time = (switch_count + DEFAULT_TIME - 1) % 10 + 1;
		display.clearDisplay();
		display.setCursor(0,0);
		display.print("Washing ");
		display.println(washing_time);
		display.print("Drying ");
		display.println(washing_time);
		display.display();
		if (digitalRead(START_BUTTON_PIN)) {
			machine_state = 1;
		}
	} else if (machine_state == 1) {
		washing(washing_time, washing_time);
		drying(0, washing_time);
		machine_state = 2;
		play_music();
	} else if (machine_state == 2) {
		if (digitalRead(START_BUTTON_PIN)) {
			display.clearDisplay();
			display.setTextColor(WHITE);
			display.setCursor(0,0);
			display.println("Finished.");
			display.display();
			delay(1000);
			display.clearDisplay();
			display.setTextColor(BLACK, WHITE);
			display.setCursor(0,0);
			display.println("Finished.");
			display.display();
			delay(1000);
		} else {
			display.clearDisplay();
			display.setTextColor(WHITE);
			delay(1000);
			machine_state = 0;
			washing_time = DEFAULT_TIME;
			switch_count = 0;
		}
	} else {
		display.clearDisplay();
		display.setTextColor(WHITE);
		display.setCursor(0,0);
		display.println("ERROR");
		display.display();
	}
}
```

# 実装結果
動作結果は下記動画をご覧いただきたい．（動画の拙さはご容赦ください．）
<iframe width="560" height="315" src="https://www.youtube.com/embed/RF-daa3RdJU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
まず電池ボックスの電源を入れるとArduinoが起動し洗濯時間調整画面へ遷移する．
タクトスイッチを押すたびに洗濯時間が1分ずつ増え，最大10分まで設定できる．
洗濯時間を設定したら，スライドスイッチを奥側に押すとモータが回転し始め"洗濯"が開始される．
なお，タイヤが取り付けられた軸が洗濯槽の回転を表しており，プロペラが取り付けられた軸は洗濯機能とは関係ないが，プロペラはギアボックスの軸がぶれないための重しの役割も担っていると思っている．（あとプロペラ良いじゃない．）
洗濯中は赤色LEDが点灯しており，脱水中は緑色LEDが点灯する．
脱水が終わるとブザーから音が流れ，OLEDディスプレイに終了通知が出る．
スライドスイッチを元に戻すと洗濯時間調整画面に戻る．

今回は簡単のため洗濯時間と乾燥（脱水）時間を同じにしたが，脱水の場合は洗濯のときよりも回転速度を向上させることで効率的に脱水できるのではないかと考えている．（実際に洗濯していないのでわからない．今後の課題とさせていただきたい．）


# 新規購入物品一覧
| 名前　| 用途 | 購入先 | 価格 | 
|:---- | :---: | :---: | :---: |  
| 工作用コード | モータドライバ結線用．結局未使用． | [Amazon](https://www.amazon.co.jp/gp/product/B00BECS17W/ref=ppx_yo_dt_b_asin_title_o04_s00?ie=UTF8&psc=1) | 160円 |
| デジタルテスター | 電圧・抵抗測定用 | [Amazon](https://www.amazon.co.jp/gp/product/B07GJ891VR/ref=ppx_yo_dt_b_asin_title_o04_s00?ie=UTF8&psc=1) | 1,273円 |
| L298Nモータドライバ | モータ制御用 | [Amazon](https://www.amazon.co.jp/gp/product/B08B87WWHV/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1) | 799円（2個セット） |
| OLEDディスプレイ | ステータス表示用 | [Amazon](https://www.amazon.co.jp/gp/product/B01M8JV310/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) | 740円 |
| 電池ボックス | 外部電源用 | [Amazon](https://www.amazon.co.jp/gp/product/B083ZYRRLQ/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1) | 398円 |
| ジャンプワイヤ（メス-オス） | モータドライバ結線用 | [Amazon](https://www.amazon.co.jp/gp/product/B00P9BVKOK/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1) | 350円 |
| ユニバーサルギアボックス | モータ回転軸・速度調整用 | [ヨドバシカメラ](https://www.yodobashi.com/product/100000001001083223/) | 555円 |
| クリップワイヤ | モータ結線用 | [ヨドバシカメラ](https://www.yodobashi.com/product/100000001000618931/) | 521円 |
| グリス | ギアボックスの速度向上・消音用 | [ヨドバシカメラ](https://www.yodobashi.com/product/100000001001052364/) | 322円 |
| プロペラ | 軸回転の可視化 | [ヨドバシカメラ](https://www.yodobashi.com/product/100000001005194130/) | 247円 |
| タイヤセット | 軸回転の可視化 | [ヨドバシカメラ](https://www.yodobashi.com/product/100000001001083266/) | 305円 |
未使用や今後も使えるものも合わせて，合計**5,670円**．...結構かかったな...
デジタルテスターは今後も他の電子工作に使用可能であり，工作用コード・プロペラ・タイヤセット・グリスは今回の工作での重要度は低いので除外，モータドライバは2個セットだったので1個だけの計上とすると，合計**2,443円**．安くなったな．よし．

# まとめと今後の課題
マスク洗濯用の簡易洗濯機を作成するため，洗濯槽の回転を制御する部分をArduinoを用いて実装した．結果として，モーターの回転速度と方向を変化させ，さらに稼働時間を調整可能な制御部を実現できた．
今後の課題は山積みであるが，最大は洗濯槽本体の作成である．洗濯槽本体には，[100円ショップで販売されているサラダスピナー](https://iemonocatalog.com/vegetable-strainer-itemlist/)がサイズと機能の両方を満たすと考えている．今後時間が取れれば作成していきたい．
**あー楽しかった！**

# 参考文献
様々なサイトを参考にしたが，ログが残っているものだけ挙げさせていただく．

- https://qiita.com/takahiro_itazuri/items/d7bc7331b196921b5880

- https://qiita.com/sugasaki/items/7df0fec82b232c41c2d2

- https://analogicintelligence.blogspot.com/2016/09/l298n-dual-h-bridge-motor-controller.html