自作全自動ニンニク乾燥機の改良

去年から本格運用を始めた全自動ニンニク乾燥機。

2019年から使い始め、ハード面・ソフト面の改良を年々続けてきました。

自動乾燥のプログラム自体は、去年の状態でほぼ完成しており、不満の無い制御をしてくれていたのですが、たまーーに謎のエラーが出て、制御そのものが止まっちゃっう事があるのですよ。

正直、これは致命的。

温度が上がりやすい時間帯に止まって、上限の35度を大きく超えるような自体になれば、下手すればニンニクそのものがダメになりかねません。

という事で、今年はエラー対策も組み込む事にしました。

エラーの原因を探ってみる

エラーがたまにしか発生しないという事は、プログラムの構文そのものが間違えている訳ではないと思われます。

そのあたりを検証すべく、たまーーーーにしか発生しないエラー文を保存して、内容確認してみました。(それぞれ長いので途中はカット)

1回目:

  • Traceback (most recent call last):
  •   File "/home/pi/python1/dh11-sheet1-4.py", line 51, in <module>
  • ・・
  • ・・・
  •     sl.s.send(struct.pack('IIII', cmd, p1, p2, 0))
  • AttributeError: 'NoneType' object has no attribute 'send'

2回目:

  • Traceback (most recent call last):
  •   File "/home/pi/python1/dh11-sheet1-4.py", line 51, in <module>
  •     wks.append_row(datas)
  • ・・
  • ・・・
  • gspread.exceptions.APIError: {'code': 400, 'message': "Unable to parse range: 'システム変更後'", 'status': 'INVALID_ARGUMENT'}

3回目:

  • Traceback (most recent call last):
  •   File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 600, in urlopen
  •     chunked=chunked)
  • ・・
  • ・・・
  • OSError: (104, 'ECONNRESET')
  • During handling of the above exception, another exception occurred:
  • Traceback (most recent call last):
  •   File "/usr/lib/python3/dist-packages/requests/adapters.py", line 449, in send
  •     timeout=timeout
  • ・・
  • ・・・
  • urllib3.exceptions.ProtocolError: ('Connection aborted.', OSError("(104, 'ECONNRESET')"))
  • ・・
  • ・・・

エラーの内容が、バラッバラな気がします^^;

エラー部分だけ抜き出してみると

AttributeError:
gspread.exceptions.APIError:
OSError:
urllib3.exceptions.ProtocolError:

正直、趣味だけでやっている身としては、エラー原因を突き止めるのはお手上げです。

頼れる人・聞く人もいないので、全て自己流ですからね。

という事で、ネットの世界に潜りながら、対策方法を調べたところ、「例外処理」という手段があったのです(いまさら)

例外処理とは?
プログラム上で予期せぬエラーや異常・環境が発生した際に(例外発生)、現在の処理を中断して、例外用に指定した処理を行う事

プログラミングをする人にとっては常識だったのかな?

私にとっては、「なるほど!」の処理でした。

Pythonの例外処理

では、実際に例外処理をやってみます。

「この例外が発生したら」→「この処理をする」
これが基本の考え方で、try・except文として処理します。

try以下に処理を記述し、例外の発生を見張ります。
その処理中に発生した例外をどうしたいかを、exceptに記述します。

また、exceptではキャッチしたい例外の種類を指定出来るのですが、今回は多岐に渡るのと、何が発生するのかよくわからないので、「全ての例外をキャッチする」方向で考えてみます。

というか、そもそも/元々KeyboardInterruptというCtrl+Cをキャッチする為にtry・except文は使っていたんですよね。

意味を知らずに使うとこういう事が起こり得るので、注意しましょう^^;

で、このKeyboardInterruptはループから抜け出すために入れていましたので、そのまま活かす方向で考えます。

その為には、exceptに基底クラスと言われるExceptionを指定します。

except Exception と記述する事で、KeyboardInterruptは除外されます。(例外処理の例外って感じ)

Pythonの組み込み例外

https://docs.python.org/ja/3/library/exceptions.html

今回の場合、例外が発生したとしても、実行中の処理を中断させたくない(継続させたい)為、処理全体をwhileによる無限ループ内に入れて、その中でtry文を記述します。

そして例外が発生した場合には、その例外の種類をprintで書き出し、再びwhileのループに入るようにします。

そして以前同様、Ctrl+CはKeyboardInterruptでキャッチして、GPIOを開放してプログラム終了・・という流れにすれば良さそうです。

無限ループする例外処理・完成版

という事で、これまでのプログラムを修正してみました。

ほぼ以前と変わりませんので、中身の細かい部分については、この記事の冒頭に貼った過去記事内で解説していますから、そちらをご覧ください。

  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. import gspread
  4. import RPi.GPIO as GPIO
  5. import dht11
  6. import time
  7. import datetime
  8. import pigpio
  9. from oauth2client.service_account import ServiceAccountCredentials
  10. # initialize GPIO
  11. GPIO.setwarnings(True)
  12. GPIO.setmode(GPIO.BCM)
  13. # read data using pin 14
  14. instance = dht11.DHT11(pin=14)
  15. ##GPIOにアクセスするためのインスタンスを作成
  16. pi = pigpio.pi()
  17. SERVO_PIN = 23
  18. while(1):
  19.     jositsu = input('除湿機運転中は10・停止中は0を入力→')
  20.     if jositsu.isdecimal():
  21.         jositsu = int(jositsu)
  22.     break
  23. key_name = './cert/raspberrypi-1-**********.json' # GoogleSheet認証キー
  24. sheet_name = 'RaspberryPi-1sheet' # シート名
  25. while True: # 全処理繰り返し
  26.     try: #エラーチェック
  27.         if __name__ == '__main__':
  28.             scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
  29.             credentials = ServiceAccountCredentials.from_json_keyfile_name(key_name, scope)
  30.             gc = gspread.authorize(credentials) # JSONキーファイルで認証
  31.             wks = gc.open(sheet_name).sheet1 # sheetをオープン
  32.         while True:
  33.             result = instance.read()
  34.             dtn = datetime.datetime.now()
  35.             while result.is_valid():
  36.                 B = result.temperature
  37.                 C = result.humidity
  38.                 D = 0.62198 * (6.1078 * 10 ** (7.5 * B / (B + 237.3)) * C / 100) / (1013.25 - (6.1078 * 10 ** (7.5 * B / (B + 237.3)) * C / 100)) * 1000
  39.                 D = round(D, 1)
  40.                 print("測定時刻: " + dtn.strftime("%Y/%m/%d %H:%M:%S"))
  41.                 print(" 気 温: " + str(B) + "C") # 元々小数点1桁で数字拾っていたので、簡単な表現に変更
  42.                 print("相対湿度: " + str(C) + "%") # 上と同じ
  43.                 print("絶対湿度: " + str(D) + "%")
  44.                 print(" 除湿機: " + str(jositsu))
  45.                 datas = [dtn.strftime("%Y/%m/%d %H:%M:%S"),B,C,D,jositsu]
  46.                 wks.append_row(datas)
  47.                 if result.temperature > 34 and jositsu == 10:
  48.                     pi.set_servo_pulsewidth(SERVO_PIN, 1650)
  49.                     time.sleep(1.0)
  50.                     pi.set_servo_pulsewidth(SERVO_PIN, 2000)
  51.                     time.sleep(1.0)
  52.                     jositsu = 0
  53.                     print("34度以上・除湿機停止")
  54.                 if result.temperature <= 33 and jositsu == 0:
  55.                     pi.set_servo_pulsewidth(SERVO_PIN, 1650)
  56.                     time.sleep(1.0)
  57.                     pi.set_servo_pulsewidth(SERVO_PIN, 2000)
  58.                     time.sleep(1.0)
  59.                     jositsu = 10
  60.                     print("33度以下・除湿機運転")
  61.                 print("---")
  62.                 time.sleep(600)
  63.                 break
  64.             else:
  65.                 print("!測定不良!" + dtn.strftime("%Y/%m/%d %H:%M:%S"))
  66.                 print("---")
  67.                 time.sleep(3)
  68.     except Exception as err: #エラーを検知した場合
  69.         print(err) #エラーをプリントする
  70.     except KeyboardInterrupt: #Ctrl+Cを検知した場合
  71.         GPIO.cleanup() #GPIOを開放
  72.         print("cleanup")
  73.         break #冒頭tryを終了
  74. print("end")

変更した部分だけ解説します。

31行目:while True:を追加し、32行目のtry以下を無限ループさせる

74行目:except Exceptionで基底クラス以外のエラーをキャッチした場合に、as err:でそのエラー内容をerrに格納する

75行目:errに格納したエラー内容をプリントする(これがないとエラーが発生したかどうか確認できない)

76~79行目:Ctrl+Cを押したら、GPIOを開放してcleanupとプリントし、32行目の無限ループから脱出する。

80行目:endとプリントしてプログラム終了

実際の運用結果

改良型を運用し始めたのが、5/31。

その後しばらく問題なく動いていたのですが、6/1の夜にとうとう例外が発生しました。

おそらく、温度・湿度測定までは出来たものの、スプレッドシートへの書き込み時に何かしらのエラーが発生した様子。

そこで、OSError:をキャッチしました。

以前ならこれでプログラムが終了していましたが、エラー内容をしっかりとプリントし、もう一度温度湿度の測定へとループしています。

合わせて、スプレッドシート側を確認すると・・

エラーが発生した23:09:23は記録されておらず、リトライして成功した23:09:29が記録されています。
(23:09:26はそもそも測定できずにリトライしている)

とりあえず一回だけしか検証できていませんが、これでいつ止まるか不安になりながら運用する事は無くなりそうです!

いやぁ、自動化最高!!

しばらくはこのまま様子を見てみましょうかね。