電子工作備忘録 ～メダカの自動給餌機～

# 完成図
自宅で飼育しているメダカのエサやりを自動化してみたいという思いから、電子工作未経験の社会人がRasberry pi picoを衝動買い。
完成・運用開始から半年ほど経過しているが製作当時の紆余曲折を思い出しながら備忘録（？）を作成する。

![IMG_1039.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/fff178a9-7ae1-cc19-f36d-553fff1ad8dc.jpeg)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/f9633cdd-81b2-6979-c307-a5e7ff28079f.png)

まずは完成品が動いている様子。

https://youtu.be/8wkqKi2rrQY

https://youtu.be/NiQyYiC0HM8

「スクリューコンベア」状のエサ排出部にすることで、排出量を回転角度によって連続的に調節できるようにしたことが、本作のこだわり。
「水槽　自動給餌機」などの検索でヒットする市販品は、エサを入れる筐体ごと回転する構造のため1回転あたりの排出量は、穴の大きさ、回転速度、エサの重さ（？）などに依存すると思われた。
今回設置対象となる水槽はメダカ4匹、フナ1匹、スジシマドジョウ1匹、ミナミヌマエビ1匹、ヒメタニシ1匹、という見た目も採食量もかなり控えめ。市販品の最小排出量でも出過ぎになってしまうのでは？という懸念が今回の工作を始めるに至ったきっかけでもあった。


# 材料と作成手順
・**エサ箱部分**
100均で買ったスパイスボトル
![IMG_0851.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/bcd1665a-f32d-0804-f65a-9bdad2f48947.jpeg)

・**スクリュー部分**
本作で最もごり押しした部分。スクリューは円形にカットした1.2mm厚のプラ板に切り込みを入れ、アイロンで熱し、冷めないうちにピンセットで引き延ばすことで作成。同じものを3つ作成し、シャフトにそって瞬間接着剤で固定。
シャフトは「タミヤ(TAMIYA) 楽しい工作シリーズ No.252 3mm六角シャフトセット」を使用
https://www.tamiya.com/japan/products/70252/index.html
エサ箱内でシャフトの位置を固定するためのストッパー（？）は「タミヤ 楽しい工作シリーズ No.240 工作ギヤセット 36T/12T」に入っていたパーツを使用。
https://www.tamiya.com/japan/products/70240/index.html
エサ箱に空いている穴ははんだごての熱とやすりで開通。
セロハンテープケースのドーナツ状の部分を、これまたはんだごてとやすりで良い感じに切り取り、エサ箱の穴にはめ込んだ。
スクリューをエサ箱に差し込み、滑らかに回転するよう、やすりで削りながら微調整。
この一連の工作は設計図も無いまま気まぐれに進めたので全く再現性が無い。二度とやりたくもない。3Dプリンターで良い感じにできないものか。
![IMG_0855.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/8307b9bb-3868-8aff-2e33-762f60a16d32.jpeg)

・**各パーツを取り付けるための土台**
100均で買った、形とサイズが良い感じのトレー
![IMG_0857.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/91c546b8-144a-e4f2-c607-d10b4785f7ba.jpeg)

・**その他もろもろ**
エサ箱の土台への取り付けは100均で買ったL字型の金具を使用、接着剤だけでは心もとなかったので針金でも固定している。
動力部は電子工作界隈では定番の（？）ステッピングモーター[28BYJ-48]。こちらは針金のみを使用して土台に固定。（瞬間接着剤を使用するとモータの内部に入り込み、動かなくなってしまった。）
モーターのシャフト（5mm径）とスクリューのシャフト（3mm径）の接続にはモーターカップリングに使用されるジョイントを使用。
https://www.amazon.co.jp/gp/product/B08XW79DR2/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&th=1
この接続を縦方向にスマートにできれば、完成した時にもっとコンパクトになるんだろうな。
![IMG_1006.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/b2e1deeb-0a5e-0fb5-dcb6-c240e8977f02.jpeg)

・**水槽への取り付け**
取り付けには100均で300円で買った、クリップ式かつぐにゃぐにゃ曲がるタイプのスマホスタンドを使用。土台トレーの背中側がちょうどスマホくらいの大きさだったので上手くフィットした。今見返すと、ブレッドボードと同じように、LEDライトに乗せるだけでも良いように見えるが、100均で閃いてしまったのだから仕方ない。
![IMG_1096.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/980e0576-2fa0-3dd7-2a15-367d06733c2d.jpeg)


# ソフトウェア部分について
今回は電子工作もプログラミングもほぼ全くの初心者であったので、ネット上の「定番」と思しき様々な情報を頼りにパーツ選び、コーディングを行った。
使用したモジュール等は以下の通り。

**マイコンボード：Rasberry Pi pico W**
使用言語はMicropython。開発環境構築など初心者がつまずきそうな点の解説記事が豊富な多いことが最大のメリット。電子工作の第一歩目としてとてもハードルが低かった。
    
**OLEDディスプレイ：SSD1306**
マイコン内で処理が終わったとか、進行中である、とかそういった情報が見えないのは初心者的に不安だったので購入。しかし結果的にはこいつのせいで発生するエラーの処理に幾度となく膨大な時間を費やした。解決してみればなんてことのないイージーミスが多かったのだが、、、
参考にさせていただいたサイトの一つは以下。
http://jh7ubc.web.fc2.com/Raspberry_Pi/Raspberry_Pi_Pico/Pi_Pico_OLED.html

**RTCモジュール：DS1307**
Rasberry Pi picoの内部時計は一度電源が切れるとリセットされてしまうらしい。最終的に時刻による自動給餌を目指している私にとってこれは無視できない仕様である。電源を入れなおすたびに時刻設定をするのは面倒くさいので。そこで、電源OFF時にも時刻が進行するリアルタイムクロックモジュールを使用することにした。しかし結果的にはこいつのせいで、、、（以下同様）
参考にさせていただいたサイトの一つは以下。
https://osorkoma.net/kumikomi/raspberry-pi-3-ds1307/



# コード
コードは体感7割くらいはChatGPTに書いてもらい、2割くらいは他記事からのコピペ、1割くらいが自分の頭を使って書いた、という感触である。冗長な箇所があったり、基本的なお作法が成ってなかったりするかもしれないが、私自身、様々な過程で素人の方が書いた備忘録的な記事に助けられたので、恥を忍んでコードをここに記すことにする。

```python:bootsel押したら回転（OLED、RTC含む。温度表示のおまけ付き。）
from machine import I2C,Pin
import utime
import ssd1306

##RTCモジュールの準備
# DS1307 RTCのI2Cアドレス
DS1307_I2C_ADDR = 0x68
# DS1307のレジスタアドレス
SECONDS_REG = 0x00
MINUTES_REG = 0x01
HOURS_REG = 0x02
DAY_REG = 0x03
DATE_REG = 0x04
MONTH_REG = 0x05
YEAR_REG = 0x06
CONTROL_REG = 0x07
# 1桁の数値を2桁の文字列に変換する関数
def format_number(number):
    return "{:02d}".format(number)

# DS1307から時刻を取得する関数
def get_time():
    i2c=I2C(0,sda=Pin(4),scl=Pin(5),freq=400000)
    time_data = i2c.readfrom_mem(DS1307_I2C_ADDR, SECONDS_REG, 7)
    seconds = (time_data[0] & 0x0F) + ((time_data[0] >> 4) & 0x07) * 10
    minutes = (time_data[1] & 0x0F) + ((time_data[1] >> 4) & 0x07) * 10
    hours = (time_data[2] & 0x0F) + ((time_data[2] >> 4) & 0x03) * 10
    day = time_data[3]
    date = (time_data[4] & 0x0F) + ((time_data[4] >> 4) & 0x03) * 10
    month = (time_data[5] & 0x0F) + ((time_data[5] >> 4) & 0x01) * 10
    year = (time_data[6] & 0x0F) + ((time_data[6] >> 4) & 0x0F) * 10 + 2000
    return (year, month, date, day, hours, minutes, seconds)

# DS1307に時刻を設定する関数
def set_time(year, month, date, day, hours, minutes, seconds):
    i2c=I2C(0,sda=Pin(4),scl=Pin(5),freq=400000)
    time_data = bytearray(7)
    time_data[0] = (seconds % 10) + ((seconds // 10) << 4)
    time_data[1] = (minutes % 10) + ((minutes // 10) << 4)
    time_data[2] = (hours % 10) + ((hours // 10) << 4)
    time_data[3] = day # 曜日（0〜6の範囲で、RTCによっては無視されることがある）
    time_data[4] = (date % 10) + ((date // 10) << 4)
    time_data[5] = (month % 10) + ((month // 10) << 4)
    time_data[6] = (year - 2000) % 10 + (((year - 2000) // 10) << 4)
    i2c.writeto_mem(DS1307_I2C_ADDR, SECONDS_REG, time_data)

week=["Sun","Mom","Tue","Wed","Thr","Fri","Sat"]

##ステッピングモーターの準備
#出力ピン設定
outA = Pin(18, Pin.OUT)  # コイル出力A
outB = Pin(19, Pin.OUT)  # コイル出力B
outC = Pin(20, Pin.OUT)  # コイル出力C
outD = Pin(21, Pin.OUT)  # コイル出力D
# 励磁パターン指定二次元配列
puls_pattern = [[1, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0],
                [0, 0, 1, 0], [0, 0, 1, 1], [0, 0, 0, 1], [1, 0, 0, 1]]
last_index = len(puls_pattern) - 1  # 励磁パターン配列の要素数を取得
pattern_number = 0  # 励磁パターン番号

#ステッピングモーター1-2相励磁 動作(1ステップ動作角度 5.625°)
def rotate (direction, angle):
    global pattern_number  # パターン番号をグローバル変数として宣言
    i = 0
    if direction == "fw" :
        while i < angle*64/5.625 :# 励磁パターン切り替え処理
            if pattern_number != last_index:  # パターン番号が励磁パターン配列の要素数でなければ
               pattern_number += 1           # パターン番号 +1
            else:                             # パターン番号が励磁パターン配列の要素数なら
               pattern_number = 0            # パターン番号0リセット
            # 出力実行
            outA.value(puls_pattern[pattern_number][0])  # パルス出力実行A〜D
            outB.value(puls_pattern[pattern_number][1])
            outC.value(puls_pattern[pattern_number][2])
            outD.value(puls_pattern[pattern_number][3])
            utime.sleep_ms(interval_time)                # 出力インターバル時間(ms)
            i = i + 1
        outA.value(0)
        outB.value(0)
        outC.value(0)
        outD.value(0)
    
    if direction == "rv" :
        while i < angle*64/5.625 :# 励磁パターン切り替え処理
            if pattern_number != 0:  # パターン番号が0でなければ
               pattern_number -= 1           # パターン番号 -1
            else:                             # パターン番号が励磁パターン配列の要素数なら
               pattern_number = last_index            # パターン番号を最終要素番号にリセット
            # 出力実行
            outA.value(puls_pattern[pattern_number][0])  # パルス出力実行A〜D
            outB.value(puls_pattern[pattern_number][1])
            outC.value(puls_pattern[pattern_number][2])
            outD.value(puls_pattern[pattern_number][3])
            utime.sleep_ms(interval_time)                # 出力インターバル時間(ms)
            i = i + 1
        outA.value(0)
        outB.value(0)
        outC.value(0)
        outD.value(0)

##温度センサーの準備
sensor_temp = machine.ADC(4)
conversion_factor = 3.3 / (65535)

#OLEDディスプレイの準備
i2c=I2C(0,sda=Pin(4),scl=Pin(5),freq=400000)
oled = ssd1306.SSD1306_I2C(128,64,i2c)


# メインループ
# 1-2相励磁の回転スピード（インターバル時間(ms)、1msが最速）
interval_time = 1

while True:
    oled.fill(0)
    reading = sensor_temp.read_u16() * conversion_factor
    year, month, date, day, hours, minutes, seconds = get_time()
    date = str(format_number(year))+"/"+str(format_number(month))+"/"+str(format_number(date))#+" "+week[day]
    time = str(format_number(hours))+":"+str(format_number(minutes))+":"+str(format_number(seconds))
       
    if rp2.bootsel_button() == 1:
        oled.text(date,0,0)
        oled.text(time,0,15)
        oled.text("Temp:" + str(round(temperature,1)), 0, 30)
        oled.text("ROTATING_fw",0,45)
        oled.show()
        #print("ROTATE_fw")
        rotate("fw", 60)
        rotate("rv", 30)
        utime.sleep(1)
    else:
        oled.text(date,0,0)
        oled.text(time,0,15)
        oled.text("Temp:" + str(round(temperature,1)), 0, 30)
        oled.show()
        utime.sleep(1)
```


# おまけ　ボツになった1号機（宇宙船型）
スクリューで仕切られた小部屋にエサが溜まり、回転によって1部屋分のエサが排出口から流下する、という構造。しかし作ってみたはいいものの、排出量の調整が離散的にしかできなかったためボツに。そんな時、「粉体輸送」「スクリューコンベア」というキーワードを知り現在の形にたどり着いたのであった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/1972f916-1ec9-9002-343a-358da4d8e5be.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/3e989ca7-262e-e470-8afc-6ebf2f767890.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3858755/60f4f5b6-39e1-ecf5-d75e-30f187117751.png)