RaspberryPiPicoで自宅トイレの自動水洗を実現

# はじめに
こんにちは！今回はトイレの話です。

ちょっと前に分譲マンションの新居を購入しました。デフォルトのトイレがタンク式で手動の水洗トイレでしたが、せっかく新築で購入したんだし、予てより自動の水洗トイレにあこがれていた感があり、水洗を何とか自動化できないかと思っていました。

トイレを自動水洗に変えるには通常は何十万円とするらしく、これができたら何十万の価値になるのだから、それをモチベーションに一肌脱ぐことにしました。新築のトイレらしくしたかったので、見た目も多少こだわりました。

本記事では、RasPiPicoを使ったプログラミング及び具体的な駆動系・センサー系の構成を紹介していきます。ちなみに、下が実際の写真。RasPiPicoは初心者で苦労しましたが、完成までこぎつけることができました！できたときは感動で何度もトイレに行きました笑

![完成写真.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787146/e75aeb2c-57d3-4de8-bfff-55eec70e2121.png)

### 前提
RasPiPicoの初期セットアップとはんだ付けは自身で行えること。写真のようなケースを作りたい場合は電動ドリル等で穴あけ加工ができること。

### 開発環境
- OS: Windows 11 64bit
- IDE: Thonny 4.0.2
- MicroPython 1.19.1?

# 準備するもの
### 本体
秋月電子で2000円程度で揃えることができた。
- シャープ測距モジュール　GP2Y0E03（I2C＆アナログ出力） 680円
- マイクロサーボ SG92R 500円
- RasberryPiPico 770円
- はんだや配線類
- それなりの強度の細い糸；釣り糸等（カビが付きにくいとよい）

### ケース
カインズに行って下記を揃えた。本記事ではケースの詳細は説明しない。
- アクリル板
- アクリルカッター
- M2ナベタッピング(6mm長)　：RasPiPico基板の固定用
- M3ナベコネジ(20mm長) with 六角ナット　：アクリル板２枚の固定用
- M3かM4サイズの円柱状スペーサー

# 自動洗浄化のおおまかな方法
　詳しくは後で述べることとして、ここでは大まかな自動洗浄の流れを述べる。まず、赤外線の距離センサーを用いることで、人が座った際に赤外線の反射する距離が変わり、人を検出することができる。そして、ある特定の条件を満たした際に、サーボモータに指令が行くようにする。サーボモータには糸が結んであり、糸はタンク内の蓋を開閉できるように配線されている。洗浄の強度はサーボモータの角度と保持時間によって調整することができる。トイレの部品に穴をあけたりして固定するのは容易ではないのと失敗したら終わりなので、基本的にモーター類などは養生テープで固定する。

# 動作仕様
センサーに非常に近くまで手を近づけたときには、非接触で自動洗浄が行われるようにする（半自動水洗モード）。具体的には
- センサーまでの距離が4cm以下にまで手を近づけ、0.5s保持した場合には半自動で大の水洗
- センサーまでの距離が4cm～60cmの場合は、検知時間に応じた自動水洗モード（下記）

センサー検出距離が上記の4cm~60cmの場合、座る時間に応じて大と小の水洗を判断して切り替えられるようにする。具体的には
- センサーが10s以下のみ人を検知し、離れたのを確認した場合には誤検知として洗浄を行わない
- センサーが10s-60sだけ人を検知し、離れたのを確認した場合には小の洗浄（人を検出したらLEDが点灯）
- センサーが60s以上検知し、離れたのを確認した場合には大の洗浄（60s以上の検出でLEDが点滅）

# タンク内のサーボモータと糸の構成
　まずは今回の肝となる機械的な部分を見ていく。下の写真のように左の洗浄ノブ（大・小と書かれた取っ手）から右に伸びる棒にはプラスチックビーズのついた鎖が繋がっている。この棒が回転することで鎖が引っ張られ、タンクの蓋が開いて水が流れるようになる。洗浄ノブとサーボモータ同士が直接干渉してしまうと、誰かが物理的に洗浄ノブを回したときにサーボモータの故障の恐れがあるため、これらは干渉しないようにする。そこで、サーボモータの駆動がタンクの蓋へのみ伝わるように、サーボモータの突起部とプラスチックビーズの鎖とを糸でつないだ。
　写真のようにタンクの中蓋に養生テープでサーボモータをしっかりと固定し、糸が張った状態にしておく。文章で書くとさっぱりしているが、実際にはこのプロセスが一番大変だった。糸をまずちょうどよい長さにしておく必要がある。糸をモータに結ぶ際が一番大変で、なかなか糸が穴に通らない！！汗がにじみ始める。不意に糸を強く引っ張ってしまうと蓋が開いて水が流れだし、下手すると周りがびたびたになる。糸の種類に関しては、手元にタコ糸があったので他の選択を考えなかったが、幅が太すぎたのと糸が乾燥しにくくカビが生えやすいので選定にちょっと失敗した。釣糸がよかったかもしれないが、今回は良しとする。ここを乗り越えれば第一関門突破。

![水洗タンク写真.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787146/a08aa904-3417-61e8-5ae2-06cc6affa2b9.png)

　サーボモータの配線はタンクの後ろ側に回す。陶器の上蓋とタンクの間には幸い隙間が設けられており、配線をつぶさなくて済んだ。サーボモータの突起部は写真のようにやや左下に傾くくらい（糸の方向と平行）がちょうどよく、このとき距離が最小となる。この状態をデフォルトの位置とし、サーボの角度を-80°と定義する。サーボモータの角度は90°から-90°まで動く。90°となるとサーボモータの突起が反対側を向き、距離が最大となるので糸が強く引っ張られることになる。
　水が流れる量 $V$ はおおよそ、タンクの蓋が開いた面積 $S$ と保持時間 $t$ の積になると思われる。
```math
L=t×S
```
タンクの蓋の開き具合 $S$ は、糸の張り具合やサーボモータの角度を60~90°の間で調整することで制御でき、時間 $t$ はサーボの保持時間で制御可能である。水の勢いはタンクの蓋の開き具合 $S$ に比例すると思われるので、「大」の場合は「小」よりサーボの角度を大きくする必要がある。

# サーボモータと距離センサーの配線
　配線図の一例を下記に示す。シャープ測距モジュールGP2Y0E03はアナログの距離測定モードとI2Cのデジタルの距離測定モードを選択できる。今回はI2Cを用いた。

![Pi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787146/a1d96981-d880-1184-20e5-3a43121288bc.png)

GP2Y0E03の取説によれば、向かって左から
1. VDD; Supply Voltage 赤
2. Vout(A); Output Terminal 白（今回は使わない）
3. GND; Ground 黒
4. VIN(IO); I/O Supply Voltage 橙
5. GPIO1; Input Terminal for Active/Stand-by Control 紫
6. SCL; I2C Clock 緑
7. SDA; I2C Data Bus 黄

となっている。図に従って配線していけばよい。No5, 6, 7に対応するピンについては、GP, SCL, SDAに対応する位置ならどこでもよい。
　サーボモータはPWM制御をするので、制御用の配線はGPに対応するピンのどこかに配線されていればよい。USBの電源コネクタを下とし、基板に対して右側にサーボの配線を伸ばしたかったので、図の左側にモータ関係のはんだ付けを行った。
　サーボモータの電源及び距離センサーの電源はまとめてUSBの5Vからとってきて、配線はVSYSにつなぐ。モーターのトルク不足が心配だったが追加の電源を付ける必要もなくUSBケーブル１本で済んだ。

# プログラミング
　プログラム自体は単純で、素直に条件分岐を書いていけばよい。I2Cクラスの部分が強いて言えば慣れないと躓くかもしれない。著者自身、I2Cのセンサーを触るのは初めてで、データのアクセスの仕方などはじめは苦労した。`I2C`クラスのドキュメントは[こちら](https://docs.micropython.org/en/latest/library/machine.I2C.html)。`I2C`インスタンス生成における初期化の第一引数はport IDのようなものに対応しており、Pinout配置図を見たときにI2C0と書かれていれば $id=0$ 、I2C1と書かれていれば $id=1$ と入力する必要があることに注意（今回は0）。`readfrom_mem()`メソッドの引数は順番にI2Cデバイスのアドレス、レジスタに保管されているデータのメモリアドレス、そして取り出すバイト数である。I2Cデバイスのアドレスは`I2C`クラスの`scan()`メソッドで確認できるので、トイレに取り付ける前にThonny上で出力してアドレスを確認しておく必要がある。メモリアドレスはシャープ測距モジュールGP2Y0E03の取説に書いてある（下図）。

![Memory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787146/c5cd2d7e-52e3-1f16-ba20-4958b462c476.png)


　距離が60cm未満の場合に人を検出したと判断するよう設定した。人によってさえぎられている間はLEDが点灯するようになっており、60sを超えると点滅にかわる。距離の測定は0.5sピッチで行われ、条件を満たしているか常に監視状態になっている（`While True`で電源を抜くまで永久ループ）。半自動洗浄モードで手を近づけて水洗する際、トイレ掃除などでセンサーから離れて立った状態の場合と、座った状態で「ああ、大便が匂うから一刻も早く流したい」と思う場合の２パターンに対して水洗できるようにした。
　サーボモータの保持時間は小便が80°で3秒、大便が90°で5秒とした。サーボの角度は最大なので、もし水量が足りないと思う場合は糸の張り具合や保持時間を調節するとよい。

```python
from machine import Pin, PWM, I2C
import utime

# 距離センサー関係
i2c = I2C(0, scl=Pin(21), sda=Pin(20), freq=100000)
print(f"address is : {i2c.scan()}") # ここに表示されたアドレスをreadfrom_memメソッドの第一引数に入れる

p1 = machine.Pin(19, Pin.OUT) # センサーのGPIOピン
p1.value(0)
p1.value(1)

led = machine.Pin(25, Pin.OUT)
led.value(0)

# サーボ関係
servo = PWM(Pin(2))
servo.freq(50)
max_duty = 65535

arg0 = 1.45/20
arg_90 = 0.5/20  # 最小0.5
arg_80 = 0.61/20
arg60 = 2.08/20
arg70 = 2.19/20
arg80 = 2.23/20
arg90 = 2.4/20  # 最大2.4

def pee():
    """ サーボの駆動； 小便に対応 """
    servo.duty_u16(int(max_duty* arg70))
    utime.sleep(1)
    servo.duty_u16(int(max_duty* arg80))
    utime.sleep(3)
    servo.duty_u16(int(max_duty* arg_80))

def poo():
    """ サーボの駆動； 大便に対応 """
    servo.duty_u16(int(max_duty* arg70))
    utime.sleep(1)
    servo.duty_u16(int(max_duty* arg90))
    utime.sleep(5) 
    servo.duty_u16(int(max_duty* arg_80))

def manual():
    """ サーボの駆動； 手を近づけて手動で水洗の場合 """
    servo.duty_u16(int(max_duty* arg70))
    utime.sleep(1)
    servo.duty_u16(int(max_duty* arg90))
    utime.sleep(4) 
    servo.duty_u16(int(max_duty* arg_80))

def calcDist(t_0=0):
    """ 距離と経過時間の計算 """
    data1 = i2c.readfrom_mem(64,0x5E,1) # 距離２桁目
    data2 = i2c.readfrom_mem(64,0x5F,1) # 距離１桁目
    
    dist = ((data1[0]*16+data2[0])/16)/4 # 10進数に変換。　Shift bit n=2（測距は64cmまで）がデフォルト。この場合は4で割る必要がある。
                                          # Shift bit n=1（測距は128cmまで）に変えるには0x35にアクセスして書き換え必要。この場合は2で割る必要がある。
    
    delta = (utime.ticks_ms() - t_0) /1000 if t_0 != 0 else 0 # 経過時間sec
    
    return dist, delta

while True:
    dist1, _ = calcDist()
    print(f"{dist1}cm")
    utime.sleep(0.5)
    
    dist2, _ = calcDist()
    print(f"{dist2}cm")
    
    if dist1 > 60 and dist2 <= 4:
        print("手を検知！半自動洗浄モード")
        led.value(1)
        manual() # サーボモーターの駆動
        led.value(0)
        
    elif dist1 < 60 and dist2 < 60:
        t_0   = utime.ticks_ms() # タイマー開始
        print("人を検知！")
        led.value(1)
        
        while True:
            utime.sleep(0.5)
            dist3, delta = calcDist(t_0)           
            print(f"Δt= {delta}s {dist3}cm")
            
            if delta >= 60:
                led.toggle() # LED点滅動作
            
            if dist3 <= 4:
                print("手を検知！半自動洗浄モード")
                manual() # サーボモーターの駆動
                led.value(0)
                
                break
            
            elif dist3 > 60:
                if 10 <= delta < 60: # 小便
                    print("小便後、人が離れたのを検知！")
                    pee() # サーボモーターの駆動
                    led.value(0)
                    
                    break
                
                elif delta >= 60: # 大便
                    print("大便後、人が離れたのを検知！")
                    poo() #
                    led.value(0)

                    break
                
                else:
                    print("誤検知として処理")
                    led.value(0)
                
                    break
            
            else:
                pass
        
    else:
        pass
```

# おわりに
　苦労はしたが何とか完成までこぎつけることができました。苦労した甲斐があった。もし興味がありましたら、トライしてみてください。初心者の方はマイコン制御の勉強にもなるかと思います。では

![完成写真.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787146/4ea3657f-3ce7-6134-416b-b5c3c996cdb6.png)