Зимовка кактусов с онлайн контролем температуры

    веб интерфейс управления температурой зимовника кактусов

    Уже много лет, как жена увлеклась разведением кактусов, а все никак ей не удавалось организовать для них правильную зимовку. Дело в том, что для кактусов очень важно, чтобы зиму они пережили при температуре от 5 до 15 °C — не ниже, чтобы не погибли, и не выше, чтобы не решили, что уже весна. Я хотел бы с вами поделиться, как весьма доступными средствами мне удалось создать систему контроля температуры на Arduino с онлайн управлением через Dropbox.

    Исходные материалы


    • Arduino Uno с макетной платой
    • Микросхема температурного сенсора LM35
    • Обогреватель типа «теплодуйка»
    • Китайский удлинитель
    • Механическое реле (5 В на катушку для управления цепью 220 В)
    • Старый ноутбук


    Обогреватель и реле


    Зимовник организован на балконе, куда не попадает солнце, поэтому там всегда прохладно. Если температура упадет ниже заданного порога, то должен включаться обогреватель, который я подключил к Arduino через механическое реле. Чтобы не разбирать обогреватель, я модифицировал китайский удлинитель:

    • Срезал и зачистил с двух сторон неиспользуемый кусочек провода «земли»
    • Подключил этот провод к шине вместо одного из «активных» проводов
    • Протянул концы обоих проводов через внутреннее отверстие и подсоединил их к реле с лицевой стороны удлинителя
    • Подсоединил к реле управляющие провода с удобным терминалом

    Теперь Arduino может управлять обогревателем, подключенным через удлинитель!

    Этапы модификации удлинителя в реле (фото)
    этапы создания реле из китайского удлинителя

    реле из китайского удлинителя
     
     

    Принципиальная схема


    принципиальная схема устройства контроля температуры зимовника кактусов

    Плата Arduino Uno подключена через USB к старому ноутбуку. В качестве температурного сенсора я использовал микросхему LM35, линейно отображающую температуру окружающей среды в напряжение.

    Для запитки реле необходим отдельный источник питания, поскольку номинальный ток катушки в 110 мА близок к предельному току выдачи Arduino Uno. Первый раз я все-таки использовал питание от Arduino Uno, но показания температурного счетчика сбивались при каждом включении реле, поэтому я организовал питание через отдельное USB-соединение.

    Обогреватель подключен к удлинителю, удлинитель — к сети питания. Обогреватель при включении реле сразу начинает греть, но на малой мощности, чтобы не пугать кактусы резкими перепадами температуры.

     

    Программа


    Программа для Arduino один раз в секунду опрашивает температурный сенсор и выдает значение температуры через последовательный интерфейс. Кроме мгновенного значения температуры, программа выдает усредненное значение и вектор состояния: режим управления обогревателем (всегда включен / всегда выключен / автоматический) и диапазон температур для автоматического режима. В этом режиме программа включает обогреватель, когда температура опускается ниже первого заданного порога, а выключает, когда поднимается выше второго заданного порога.

    Программа для 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 через последовательный интерфейс и каждые десять минут добавляет в файл cactuslog.txt усредненную температуру и вектор состояния устройства. В лог попадает также точное время включения и выключения обогревателя. Если программа обнаруживает командный файл cactuscmd.txt, то содержимое этого файла несколько раз посылается Arduino через последовательный интерфейс, а сам файл переименовывается в cactusini.txt. Этот командный файл выполняется один раз при старте программы, поэтому если будет отключение электричества и перезагрузка системы, то через этот файл она восстановит свое исходное состояние.

    Программа на 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. Одна программа на Python запущена на старом ноутбуке, соединенном с Arduino, и работает с логами и командами как с локальными файлами. Другая программа на Python запускается из той же папки на любом компьютере и создает простой HTTP сервер с заданным адресом и портом. Понадобится установка нескольких библиотек для Python: SciPy и dateutil.

    Запустив вторую программу, можно следить за температурой в зимовнике прямо из браузера! Сгенерированная страница отображает:

    • сглаженный график температур за последние трое суток
    • пределы изменения температур за последнюю неделю
    • периоды включения обогревателя
    • текущий режим работы системы с возможностью его изменения

    Еще раз посмотреть график
    веб интерфейс управления температурой зимовника кактусов

    Благодаря 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


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


    UPDATE


    • Перевел проект на Arduino Nano, что весьма улучшило эстетику и лаконичность сборки
    • Добавил цифровой датчик влажности DHT-22. Заодно убедился, что температура, измеряемая датчиком, приблизительно равна показаниям LM35, который остался основным датчиком температуры
    • Обнаружил причину, по которой сбивались показания датчика температуры при включении обогревателя: ток реле поднимал землю датчика. Исправил тем, что использовал два разных входа земли контроллера для реле и датчика. Второе питание больше не нужно!
    • Существенно улучшил алгоритм сглаживания графиков

    Поделиться публикацией
    Никаких подозрительных скриптов, только релевантные баннеры. Не релевантные? Пиши на: adv@tmtm.ru с темой «Полундра»

    Зачем оно вам?
    Реклама
    Комментарии 32
    • 0
      А что плохого чтобы кактусы решили раньше времени что весна пришла?
      • +7
        Период покоя в сухости и холоде нужен, чтобы кактусы накопили силы для цветения, и чтобы не росли зимой тощими и бледными из-за недостатка света.
        (с) жена
        • 0
          Мне тут аналогично жена подсказывает, что дуйкой может быть не совсем правильно, ибо контролировать надо в основном корневую систему, а не стволы ) А с дуйкой не факт, что она прогреется.
          • 0
            Я надеюсь на то, что если температура воздуха будет в нужных пределах, то температура почвы тоже будет в нужных пределах.
      • +4
        Мне одному кажется что ноутбук в этой схеме лишний? Из пушки по воробьям получается. Можно же было взять контроллер с wifi и отправлять файлик в dropbox прямо с него.
        • +4
          Это пылившийся на полке и неиспользуемый уже много лет ноутбук 2006 года — другого применения для него я не вижу. Возможно, что это будет домашний сервер, чтобы освободить мой рабочий ноут.
        • +5
          На основании даташита я делаю вывод, что диод в реле — отсутствует. Поставьте диод параллельно обмотке реле в обратной полярности, иначе когда-нибудь у вас транзистор помрет и контроллер за собой потащит.
          • 0
            Спасибо огромное! Вы абсолютно правы, диода нет, надо поставить!
            • +2
              Надо было там, где такие модные реле брали, вот подобную штуку еще прихватить:
              image
              Как раз для таких сокетов.
            • –2
              Можете пояснить, зачем он там?
              • +5
                Маленький экскурс в физику:
                Обмотка с током — это такой накопитель энергии. После того, как ток через нее прерывается, она старается эту энергию выбросить, давая мощный импульс энергии. Чтобы этот импульс не пошел обратно по цепям убивать транзистор, мы устраиваем ему короткое замыкание на диоде.

                Подробности и водопроводные аналогии
                Конкретно о ситуации с реле
              • +1
                Добавил диод на балконе и в принципиальной схеме.
              • 0
                Судя по фотографии, теплодуйка еще и ардуино с удлинителем греет.
                • +1
                  Теплодуйка на самом деле стоит в другом месте — это для кадра мы поставили ее рядом с контроллером.
                • +2
                  Позанудствую, но будет полезно, на будущее.
                  если память не изменяет, у LM35 на выходе 10mV на градус, начиная с 0 по Кельвину. У вас окно с 5-15 градусов, на выходе сенсора это от 2.78V до 2.88V, встроенный ИОН у AVR или 1.1V или 2.56V внешнего у вас нет, значит используете напряжение питание как Vref, а значит получите 4.88mV на единицу измерения и 20 единиц на все ваше температурное окно и это составит чуть менее 2% от возможного диапазона и примерно соответсвует погрешности измерения. Т.е. ваш прибор не термометр, а показометр — он что-то показывает.
                  Для точности лучше использовать датчики с цифровым интерфейсом (DS18B20) или к LM35 применить немного рассыпухи и растянуть измеряемые 100mV до всех 5V.
                  • 0
                    У LM35 10 mV на градус Цельсия, причем при таком подключении измеряется температура от +2°C до +150°C. АЦП при Vref, равном напряжению питания, позволяет измерять с погрешностью 5 мВ, т.е. плюс-минус полградуса при погрешности микросхемы в 0.25 градуса. Пожалуй, можно смело делать Vref 1.1V и собирать урожай точности!
                    • 0
                      Обновил код — теперь измерения в пределах погрешности микросхемы!
                      • +2
                        Вот тут, в статье про Аналоговый реобас DIHALT прекрасно расписал вариант работы с LM35 почитайте, ничего сложного, один корпус LM358 и погрешность ADC не будет играть никакой роли.
                        Да, кстати, для подобных термостатов, вместо arduino и вообще микроконтроллеров, можно обойтись одной единственной микросхемой DS1821. Правда красивых графиков не будет.
                    • +5
                      Задумка хорошая, сам в этом году наконец-то реализовал зимовку на неотапливаемом балконе. Без контроля температуры с помощью Ардуино, но… может пригодится совет. Я взял аквариум, не сильно большой, где-то на 1/3 засыпал дренажом, внутри которого уложил нагревающий кабель для террариумов (90W). Все это заведено на обычный «механический» термостат, на котором выставлено что-то около 10 градусов. Сейчас болтается в районе нуля снаружи, термостат эпизодически врубается, прогревает грунт, который постепенно отдает тепло внутрь аквариума. Средняя температура ночью — около 10-11 градусов, днем прогревается больше, если не убрать полотенце, которым сверху аквариум закрыт… )) Если убрать — то где-то 10-13.
                      Контроль температуры с помощью простого электронного градусника ) Надо действительно прикрутить что-то, чтобы само снимало и логировало данные.

                      • +2
                        Оклейте пенофолом.
                        • 0
                          Была такая мысль, может быть частично (дно, стенка у борта балкона) обклею, но в основном, оставил как есть, чтобы дневной свет попадал.
                      • +1
                        Спасибо за статью.
                        А еще мне нравится ваш ковролин на балконе!
                        • +1
                          Не боитесь оставлять нагреватель без присмотра, да ещё на ковре?
                          • 0
                            Я думал насчет безопасности, но именно эта теплодуйка имеет встроенную защиту от перегрева. Однажды, еще до кактусов, она себя проявила и выключилась на несколько часов. А вообще — если отображаемая температура поднимется до 451°F — значит, беда!
                            • +1
                              Шутка про 451 градус конечно же интересная, но увы — не сработает:
                              451 градус шкалы Фаренгейта — это примерно 233 градуса шкалы Цельсия. Даташитна LM35 утверждает, что верхняя граница рабочей температуры для него 150 градусов шкалы Цельсия.

                              Для измерения столь высоких температур полупроводниковые датчики не применимы — работа полупроводников нарушается при температурах, немногим превышающих сотню градусов (по Цельсию). Для такой жары уместны различные термопары, например — серии ТХА (использованы сплавы хромель и алюмель) — диапазон рабочих температур от -270 до 1370 градусов Цельсия.
                          • +1
                            А кактусы разводите чтоб от вредного излучения компьютеров защищаться?
                            • +1
                              Нет, для этого у меня есть специальная шапочка из фольги с заземлением на батарею =)
                            • +2
                              Ничего не понял. А почему не купить термореле, которое будет установлено на 10 градусов?
                              Например, www.ukrrele.com/thermo-regulator.htm
                              Я такой использую для приготовления йогурта, температура должна быть не выше 42С
                              • 0
                                Я думаю поставить такой же на даче, которая довольно далеко от дома, чтобы можно было наблюдать за температурой сидя в уютном кресле.

                                Иными словами, самое ценное в этом проекте — обратная связь.
                                • 0
                                  А за ссылку спасибо! Удобный и с розеткой.
                                • +1
                                  <<< UPDATE >>>
                                  • Перевел проект на Arduino Nano, что весьма улучшило эстетику и лаконичность сборки
                                  • Добавил цифровой датчик влажности DHT-22. Заодно убедился, что температура, измеряемая датчиком, приблизительно равна показаниям LM35, который остался основным датчиком температуры
                                  • Обнаружил причину, по которой сбивались показания датчика температуры при включении обогревателя: ток реле поднимал землю датчика. Исправил тем, что использовал два разных входа земли контроллера для реле и датчика. Второе питание больше не нужно!
                                  • Существенно улучшил алгоритм сглаживания графиков
                                  • 0
                                    у меня другая проблема в квартире была — как понизить температуру зимовки…
                                    выход оказался до банального простым (правда требующим большого холодильника)

                                    короче, кактусы готовим к зиме (не поливаем месяц), потом в картонные коробки и в холодильник на пару месяцев.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.