オンライン温度制御によるサボテン越冬

サボテン冬休眠ウェブインターフェース



何年もの間、私の妻はサボテンの繁殖が好きでしたが、それでも彼女は彼らのために正しい冬を組織することができませんでした。 事実は、サボテンにとって、彼らが5〜15°Cの温度で冬を生き抜くことが非常に重要であるということです-低くならないように、死なないように、そして高くないように、それが春であると決定しないように。 Dropboxを介したオンライン制御でArduinoに温度制御システムを作成することができたということを、非常に手頃な価格で提供できることを皆さんと共有したいと思います。



原材料







ヒーターとリレー



冬の道路は、太陽が落ちないバルコニーに配置されているため、常に涼しいです。 温度が所定のしきい値を下回った場合、ヒーターがオンになり、機械式リレーを介してArduinoに接続します。 ヒーターを分解しないように、中国の延長コードを変更しました。





Arduinoは、延長コードを介して接続されたヒーターを制御できるようになりました!



リレーの延長コードの修正段階(写真)
中国の延長ケーブルからリレーを作成する手順



中国の延長コードからのリレー







回路図



冬のサボテンの温度制御装置の模式図



Arduino Unoボードは、USBを介して古いラップトップに接続されています。 温度センサーとして、周囲温度を電圧で直線的に表示するLM35チップを使用しました。



110 mAの定格コイル電流はArduino Unoの最大出力電流に近いため、リレーに電力を供給するために別個の電源が必要です。 初めてArduino Unoの電源を使用しましたが、リレーがオンになるたびに温度計が混乱していたため、別のUSB接続で電源を整理しました。



ヒーターは延長コードに接続され、延長コードは電源に接続されます。 リレーがオンになると、ヒーターはすぐに温まり始めますが、温度が急激に変化してもサボテンが怖がらないように、低電力です。





プログラム



Arduinoプログラムは、温度センサーを1秒に1回ポーリングし、シリアルインターフェイスを介して温度値を提供します。 瞬時温度値に加えて、プログラムは平均値と状態ベクトルを提供します:ヒーター制御モード(常にオン/常にオフ/自動)および自動モードの温度範囲。 このモードでは、プログラムは温度が最初に設定されたしきい値を下回るとヒーターをオンにし、2番目に設定されたしきい値を超えるとヒーターをオフにします。



Arduino用プログラム
/////////////////////////////////////////////////////////////////////////////// // Cactus Tracker v1.0.1 / December 8, 2014 // by Maksym Ganenko <buratin.barabanus at Google Mail> /////////////////////////////////////////////////////////////////////////////// const int PIN_HEATER = 10; const int DELAY_MS = 1000; const int MAGIC = 10101; const float TEMP_MAX = 20.0; enum { OFF = 0, ON, AUTO }; int mode = AUTO; float tempAverage = NAN; bool heater = false; float heaterFrom = 5.f; float heaterTo = 10.f; void startHeater() { digitalWrite(PIN_HEATER, HIGH); heater = true; } void stopHeater() { digitalWrite(PIN_HEATER, LOW); heater = false; } void setup() { Serial.begin(9600); digitalWrite(PIN_HEATER, LOW); pinMode(PIN_HEATER, OUTPUT); analogReference(INTERNAL); for (int i = 0; i < 100; ++i) { analogRead(A0); } } void loop() { float tempMV = float(analogRead(A0)) / 1024 * 1.1; float tempCurrent = tempMV / 10e-3; if (isnan(tempAverage)) { tempAverage = tempCurrent; } else { tempAverage = tempAverage * 0.95f + tempCurrent * 0.05f; } if (Serial.available()) { if (Serial.parseInt() == MAGIC) { int newMode = Serial.parseInt(); float newHeaterFrom = Serial.parseFloat(); float newHeaterTo = Serial.parseFloat(); if (newMode >= OFF && newMode <= AUTO && newHeaterFrom < newHeaterTo) { mode = newMode; heaterFrom = newHeaterFrom; heaterTo = newHeaterTo; stopHeater(); } } } bool overheat = tempAverage >= TEMP_MAX; if (!overheat && (mode == ON || (mode == AUTO && tempAverage <= heaterFrom))) { startHeater(); } if (overheat || mode == OFF || (mode == AUTO && tempAverage >= heaterTo)) { stopHeater(); } Serial.print("mode = "); Serial.print(mode); Serial.print(", tempCurrent = "); Serial.print(tempCurrent); Serial.print(", tempAverage = "); Serial.print(tempAverage); Serial.print(", heater = "); Serial.print(heater); Serial.print(", heaterFrom = "); Serial.print(heaterFrom); Serial.print(", heaterTo = "); Serial.println(heaterTo); delay(DELAY_MS); } ///////////////////////////////////////////////////////////////////////////////
      
      





Pythonは、 pySerialライブラリがインストールされた古いラップトップにインストールされます。 Pythonプログラムはシリアルインターフェイスを介してArduinoに接続し、10分ごとに平均温度とデバイス状態ベクトルをcactuslog.txtファイルに追加します。 ログには、ヒーターのオン/オフの正確な時刻も含まれます。 プログラムがバッチファイルcactuscmd.txtを検出すると、このファイルの内容がシリアルインターフェイスを介してArduinoに数回送信され、ファイル自体の名前がcactusini.txtに変更されます 。 このバッチファイルはプログラムの開始時に1回実行されるため、停電やシステムの再起動が発生した場合、このファイルを介して元の状態に戻ります。



古いラップトップ用のPythonプログラム
 ############################################################################### # Cactus Tracker v1.0.1 / December 8, 2014 # by Maksym Ganenko <buratin.barabanus at Google Mail> ############################################################################### import serial, re import sys, os, traceback from datetime import datetime # arduino serial port in your system SERIAL = (sys.platform == "win32") and "COM4" or "/dev/tty.usbmodem1421" # input / output files INIFILE = "cactusini.txt" CMDFILE = "cactuscmd.txt" LOGFILE = "cactuslog.txt" # log update period in seconds UPDATE_PERIOD_SEC = 600 ############################################################################### def execute(cmdfile, **argv): if os.path.isfile(cmdfile): try: # input fcmd = open(cmdfile) stream.write(((fcmd.read().strip() + " ") * 10).strip()) fcmd.close() if "renameTo" in argv: dstfile = argv["renameTo"] if os.path.isfile(dstfile): os.remove(dstfile) os.rename(cmdfile, dstfile) except: traceback.print_exc() if fcmd and not fcmd.closed: fcmd.close() firstRun = True fcmd, flog, timemark, lastState = None, None, None, None stream = serial.Serial(SERIAL, 9600) while True: s = stream.readline() if "mode" in s: record = dict(re.findall(r"(\w+)\s+=\s+([-.\d]+)", s)) mode, temp = int(record["mode"]), float(record["tempAverage"]) heater = int(record["heater"]) heaterFrom = float(record["heaterFrom"]) heaterTo = float(record["heaterTo"]) state = (mode, heater, heaterFrom, heaterTo) if firstRun: execute(INIFILE) firstRun = False execute(CMDFILE, renameTo = INIFILE) timeout = not timemark or \ (datetime.now() - timemark).seconds > UPDATE_PERIOD_SEC if timeout or state != lastState: output = (datetime.now(), temp, mode, heater, heaterFrom, heaterTo) output = "%s,%.2f,%d,%d,%.1f,%.1f" % output try: # output flog = open(LOGFILE, "a") flog.write(output + "\n") except: traceback.print_exc() if flog: flog.close() print output timemark = datetime.now() lastState = state ###############################################################################
      
      







視覚化とDropbox



プロジェクト全体がDropboxに追加された1つのフォルダーに収まります。 1つのPythonプログラムは、 Arduinoに接続された古いラップトップで実行され、ローカルファイルとしてログとコマンドを処理します。 別のPythonプログラムが任意のコンピューターの同じフォルダーから実行され、指定されたアドレスとポートで単純なHTTPサーバーを作成します。 Python用のいくつかのライブラリSciPydateutilをインストールする必要があります。



2番目のプログラムを実行すると、冬の小屋の温度をブラウザから直接監視できます! 生成されたページが表示されます:





もう一度チャートを見る
サボテン冬休眠ウェブインターフェース






Dropboxのおかげで、このプロジェクトは自宅だけでなく、たとえば国内でも開始できます。 Dropbox自体がすべてのファイルを同期し、プログラムはローカルファイルのみを扱うかのように記述されます。 唯一のことは、停電の可能性に注意し、コンピューターを再起動する必要があるということです。



冬を表示および管理するためのPythonプログラム
 ######################################################################################### # Cactus Tracker v1.0.5 / January 11, 2015 # by Maksym Ganenko <buratin.barabanus at Google Mail> ######################################################################################### import io, os, re, traceback import BaseHTTPServer, urlparse, base64 import dateutil.parser import matplotlib, numpy from matplotlib import pylab from matplotlib.ticker import AutoMinorLocator from matplotlib.colors import rgb2hex from datetime import datetime, timedelta from itertools import groupby HOST = "stepan.local" PORT = 8080 USERNAME = "cactus" PASSWORD = "forever" LOGFILE = "cactuslog.txt" CMDFILE = "cactuscmd.txt" FONT = "Arial" FONT_SIZE = 12 STATS_DAYS_NUM = 7 SMOOTH_WINDOW = 9 CURVE_ALPHA = [1.0, 0.5, 0.25, 0.1] MAGIC = 10101 # time difference in seconds between real time and log time LOG_TIME_OFFSET_SEC = 3600 OFF, ON, AUTO = 0, 1, 2 ######################################################################################### class CactusHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(self): if not self.authorize(): return url = urlparse.urlparse(self.path) query = urlparse.parse_qs(url.query) pending, smooth = False, SMOOTH_WINDOW if "mode" in query and "hfrom" in query and "hto" in query: pending = True try: mode = int(query["mode"][0]) heaterFrom = float(query["hfrom"][0]) heaterTo = float(query["hto"][0]) self.update_params(mode, heaterFrom, heaterTo) except: traceback.print_exc() if "smooth" in query: try: smooth = int(query["smooth"][0]) except: traceback.print_exc() if self.path in [ "/cactus.png", "/favicon.ico" ]: self.send_image(self.path) else: self.send_page(pending, smooth) self.wfile.close() def authorize(self): if self.headers.getheader("Authorization") == None: return self.send_auth() else: auth = self.headers.getheader("Authorization") code = re.match(r"Basic (\S+)", auth) if not code: return self.send_auth() data = base64.b64decode(code.groups(0)[0]) code = re.match(r"(.*):(.*)", data) if not code: return self.send_auth() user, password = code.groups(0)[0], code.groups(0)[1] if user != USERNAME or password != PASSWORD: return self.send_auth() return True def send_auth(self): self.send_response(401) self.send_header("WWW-Authenticate", "Basic realm=\"Cactus\"") self.send_header("Content-type", "text/html") self.end_headers() self.send_default() self.wfile.close() return False def send_default(self): self.wfile.write(""" <html> <body style="background:url(data:image/png;base64,{imageCode}) repeat;"> </body> </html>""".format(imageCode = "iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAA" + "AJ0lEQVQIW2NkwA7+M2IR/w8UY0SXAAuCFCNLwAWRJVAEYRIYgiAJALsgBgYb" + "CawOAAAAAElFTkSuQmCC")) def address_string(self): host, port = self.client_address[:2] return host def update_params(self, mode, heaterFrom, heaterTo): if max(mode, heaterFrom, heaterTo) >= MAGIC: print "invalid params values" return fout = open(CMDFILE, "w") fout.write("%d %d %.1f %.1f" % (MAGIC, mode, heaterFrom, heaterTo)) fout.close() def send_image(self, path): filename = os.path.basename(path) name, ext = os.path.splitext(filename) fimage = open(filename) self.send_response(200) format = { ".png" : "png", ".ico" : "x-icon" } aDay = timedelta(days = 1) now = datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT') expires = (datetime.now() + aDay).strftime('%a, %d %b %Y %H:%M:%S GMT') self.send_header("Content-type", "image/" + format[ext]) self.send_header("Cache-Control", "public, max-age=" + str(aDay.total_seconds())) self.send_header("Date", now) self.send_header("Expires", expires) self.send_header("Content-length", os.path.getsize(filename)) self.end_headers() self.wfile.write(fimage.read()) fimage.close() def fix_time(self, X): time = X[0].timetuple() if time.tm_hour == 0 and time.tm_min <= 11: X[0] -= timedelta(seconds = time.tm_min * 60 + time.tm_sec) time = X[-1].timetuple() if time.tm_hour == 23 and time.tm_min >= 49: offset = (60 - time.tm_min - 1) * 60 + (60 - time.tm_sec - 1) X[-1] += timedelta(seconds = offset) def make_smooth(self, Y, winSize): winSize = min(winSize, len(Y) - 2) if winSize == 0: return list(Y) Y = [ 2 * Y[0] - foo for foo in reversed(Y[1:winSize + 1]) ] + list(Y) \ + [ 2 * Y[-1] - foo for foo in reversed(Y[-winSize - 1:-1]) ] window = numpy.ones(winSize * 2 + 1) / float(winSize * 2 + 1) Y = numpy.convolve(Y, window, 'same') Y = Y[winSize:-winSize] return list(Y) def send_page(self, pending, smooth): self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() data, flog = [ ], None nowDate = datetime.now().date() while not flog: try: flog = open(LOGFILE) except: traceback.print_exc() mode, heater, heaterFrom, heaterTo = AUTO, 0, 5, 10 for s in flog: row = tuple(s.strip().split(",")) offset = timedelta(seconds = LOG_TIME_OFFSET_SEC) date = dateutil.parser.parse(row[0]) + offset temp = float(row[1]) if len(row) == 3: heater = int(row[2]) elif len(row) >= 3: mode, heater = int(row[2]), int(row[3]) heaterFrom, heaterTo = float(row[4]), float(row[5]) data.append((date, temp, heater)) stats = [ ] matplotlib.rc("font", family = FONT, size = FONT_SIZE) fig = pylab.figure(figsize = (964 / 100.0, 350 / 100.0), dpi = 100) ax = pylab.axes() for date, points in groupby(data, lambda foo: foo[0].date().isoformat()): X, Y, H = zip(*points) deltaDays = (nowDate - X[0].date()).days if deltaDays >= STATS_DAYS_NUM: continue if len(X) == 1: continue # convert to same day data alpha = CURVE_ALPHA[min(len(CURVE_ALPHA) - 1, deltaDays)] tempColor = rgb2hex((1 - alpha, 1 - alpha, 1)) heaterColor = rgb2hex((1, 1 - alpha, 1 - alpha)) X = [ datetime.combine(nowDate, foo.time()) for foo in X ] self.fix_time(X) if deltaDays < len(CURVE_ALPHA) - 1: # make smooth and draw start = 0 for heater, group in groupby(zip(Y, H), lambda foo: foo[1]): finish = start + len(list(group)) XS = X[start:finish + 1] if heater: YS = Y[start:finish + 1] elif finish + 1 - start < smooth: winSize = (finish + 1 - start) / 2 YS = self.make_smooth(Y[start:finish + 1], winSize) else: YS = self.make_smooth(Y[start:finish + 1], smooth) pylab.plot(XS, YS, linewidth = 2, color = heater and heaterColor or tempColor) start = finish else: for i in range(3): Y = self.make_smooth(Y, smooth) self.fix_time(X) stats.append((X, Y)) # plot stats curve if deltaDays == len(CURVE_ALPHA) - 1: X0, Y0 = stats.pop(0) for curve in stats: X1, Y1 = curve pylab.fill(X0 + list(reversed(X1)), Y0 + list(reversed(Y1)), color = tempColor) ax.xaxis_date() ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%H:%M")) ax.xaxis.set_major_locator(matplotlib.dates.HourLocator()) ax.yaxis.get_major_locator().set_params(integer = True, nbins = 11) ax.xaxis.grid(True, "major") ax.yaxis.grid(True, "major") ticks = ax.yaxis.get_major_locator().bin_boundaries(*ax.get_ylim()) if len(ticks) >= 2 and round(ticks[1] - ticks[0]) > 1: step = int(round(ticks[1] - ticks[0])) ax.yaxis.grid(True, "minor") ax.yaxis.set_minor_locator(AutoMinorLocator(n = step)) ax.tick_params(axis = "both", which = "both", direction = "out", labelright = True) ax.tick_params(axis = "x", which = "major", labelsize = 8) ax.grid(which = "major", alpha = 1.0) fig.autofmt_xdate() pylab.tight_layout() image = io.BytesIO() pylab.savefig(image, format = "png") pylab.clf() image.seek(0) graph = "<img src='data:image/png;base64,%s'/>" % \ base64.b64encode(image.getvalue()) image.close() pending = pending or os.path.isfile(CMDFILE) self.wfile.write(re.sub(r"{\s", r"{{ ", re.sub(r"\s}", r" }}", """ <html> <head> <title>Cactus Tracker</title> <meta http-equiv="refresh" content="{pending};URL='/'"> <style> body { font-family: {font}, sans-serif; font-size: {fontSize}pt; width: 964px; margin: 47px 30px 0 30px; padding: 0; background-color: white; color: #262626; } h1 { font-size: 24pt; margin: 0; padding-bottom: 4px; border-bottom: 2px dotted #262626; margin-bottom: 26px; } p { margin-left: 38px; margin-bottom: 20px; } input { font-family: {font}, sans-serif; font-size: {fontSize}pt; border: 2px solid #262626; padding: 2px 6px; } button { font-family: {font}, sans-serif; font-size: {fontSize}pt; padding: 4px 8px; border: 2px solid #262626; border-radius: 10px; background-color: white; color: #262626; margin: 0 3px; } form { display: inline-block; margin: 0; } .selected, button:hover:not([disabled]) { cursor: pointer; background-color: #262626; color: white; } .selected:hover { cursor: default; } .heater { width: 50px; text-align: center; margin: 0 3px; } .pending { opacity: 0.5; } .hidden { display: none; } </style> </head> <body> <h1>Cactus Tracker</h1> <div>{graph}</div> <table style="width: 100%;" cellspacing=0 cellpadding=0> <tr> <td align=left> <form action="/" class="{transparent}"> <p>Heater: <button type="submit" name="mode" class="{modeOn}" value="1" {disabled}> on </button> <button type="submit" name="mode" class="{modeOff}" value="0" {disabled}> off </button> <button type="submit" name="mode" class="{modeAuto}" value="2" {disabled}> auto </button> <input type="hidden" name="hfrom" value="{heaterFrom:.0f}"/> <input type="hidden" name="hto" value="{heaterTo:.0f}"/> </form> <form action="/" class="{transparent} {heaterAuto}"> <span style="margin-left: 30px;"> <input type="hidden" name="mode" value="{mode}"/> heat from <input name="hfrom" class="heater" maxlength=2 value="{heaterFrom:.0f}" {disabled}/> to <input name="hto" class="heater" maxlength=2 value="{heaterTo:.0f}" {disabled}/> °C <button type="submit" style="visibility: hidden;" {disabled}></button> </span> </form> </td> <td style="opacity: 0.5;" align=right> <span style="margin-right: 40px;">The last {days} days are shown</span> </td> </tr> </table> </div> <div style="position: absolute; top: 7px; left: 760px;"> <img src="cactus.png"> </div> </body> </html> """)).format( font = FONT, fontSize = FONT_SIZE, days = STATS_DAYS_NUM, graph = graph, mode = mode, heaterFrom = heaterFrom, heaterTo = heaterTo, modeOff = (mode == OFF) and "selected" or "", modeOn = (mode == ON) and "selected" or "", modeAuto = (mode == AUTO) and "selected" or "", pending = pending and "20" or "1200", disabled = pending and "disabled=true" or "", transparent = pending and "pending" or "", heaterAuto = (mode != AUTO) and "hidden" or "")) ######################################################################################### server = BaseHTTPServer.HTTPServer((HOST, PORT), CactusHandler) server.serve_forever() #########################################################################################
      
      







参照資料







Arduinoの温度制御されたサボテンの冬の写真




/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / \




更新








All Articles