Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 1. Используем Arduino Uno R3.

20160625_122911Внезапно, в порыве очередной энергетической оптимизации загородного хозяйства, мне в голову пришла интересная мысль. Мне захотелось понимать какой расход природного газа происходит на моем деревенском объекте. Что-то счета за голубое топливо, в последнее время, начали заставлять задумываться о его экономии. А эффективно экономить можно только тогда, когда понимаешь где у тебя происходит нецелевое расходование ресурса.

Собственно, в моем случае источников потребления природного газа на вверенном под мое управление объекте всего два. Это напольный газовый котел с автоматикой Siemens и газовая плита. Если к плите никаких вопросов нет, она жжет топливо только по приказу человека, то к котлу возникает их множество.

С одной стороны, инженеры немецкого промышленного гиганта знают свою работу на отлично. Автоматика, несмотря на годы, работает и управляет потреблением газа вполне сносно. Но вот, с другой стороны. С другой, логика потребления газа котлом мне далеко не всегда понятна. Иногда в жуткую жару он может весьма активно сжечь несколько десятков литров дорого топлива и не поперхнуться. А невнятная документация, требующая для своего понимания, дополнительного обучения на специализированных курсах, еще больше подливает масла в огонь.

В общем, решил я собирать статистику потребления газа. Благо счетчик газовый установлен в моем помещении, да еще и оборудован дополнительным счетчиком импульсов, который, по идее, должен был передавать сведения в газовую компанию, но последняя на этот факт просто забила. Осталось только подключить к счетчику импульсов микроконтроллер, считать им импульсы и передавать их в какой-нибудь сервис или складывать в базу данных, дабы имелась возможность для проведения последующего статистического анализа.

Итак, дано:

  1. Счетчик газовый мембранный марки BK-G6 производства Elster. Впрочем, марка счетчика большой роли не играет, важен счетчик импульсов.
  2. Счетчик импульсов IN-Z61 все от той же Elster. Впрочем, марка счетчика тоже большой роли не играет, поскольку все они, за редким исключением, выполнены в виде магнитного реле, читай «геркон», с защитой от внешнего магнитного поля. А все остальное работает идентично. На выходе всего несколько проводков.

Немного информации по счетчику импульсов газового счетчика.

Схема подключения счетчика импульсов

Схема подключения счетчика импульсов IN-Z61

Счетчик импульсов в силу своей простоты имеет всего четыре провода для подключения, но нас интересует всего два: зеленый и коричневый. Ресурс чувствительного элемента в нем составляет 20.000.000 импульсов, что мне кажется весьма и весьма много, хотя, может быть, производитель и обманывает. Максимальное напряжение 24 В постоянного тока, с максимальным током до 50 мА. Напомню, что максимальная нагрузка на порт в Arduino всего 40 мА. Минимальное время замкнутого контакта внутри счетчика 0.25 секунды, а сопротивление всего 0.5 Ома.

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

Желаемые функции

Помимо простого чтения импульсов со счетчика импульсов, я решил отправлять их на сервер ThingSpeak. Где удобно создавать графики из полученных данных, проводить исследования или же применять мощную визуализацию:

  1. Считать импульсы от счетчика импульсов газового счетчика.
  2. Отправлять считанные импульсы через интернет на сервер ThingSpeak.

Но ведь подсчет импульсов да их отправка в далекие дали совсем не загрузит микроконтроллер под завязку. Нужны дополнительные функции. Поскольку газовый счетчик, а, следовательно, и микроконтроллер, установлены в котельной, где подразумевается наличие котла, то хорошо бы иметь функции по контролю за утечкой природного газа и превышению уровня СО. Микроконтроллер должен уметь:

  1. Контролировать и сигнализировать об утечке природного газа.
  2. Контролировать и сигнализировать уровень CO.

Котельная в моем строении является частью гаража. И хочется, особенно зимой, понимать, что там в этом гараже происходит в плане температуры. А поскольку распространенные датчики температуры объединены еще и с гигрометрами, то можно одновременно получать сведения о влажности в помещении. Но если уж пошла такая пьянка, то имеет смысл заодно контролировать температуру и влажность вне помещения. Благо отделены они всего-то одной стеной:

  1. Контроль температуры и влажности в помещении.
  2. Контроль температуры и влажности вне помещения.

Ну и в качестве служебных функций просто необходима светодиодная сигнализация. Как минимум нужно отображать, что устройство работает. Нужно сигнализировать о пойманных импульсах, оповещать потребителя об опасных уровнях газа в воздухе и мигать об ошибках, возникших в процессе работы.

  1. Индикатор работоспособности устройства.
  2. Индикатор импульсов газового счетчика.
  3. Индикатор ошибок.
  4. Индикатор опасных концентраций газа в воздухе.

В результате получилось десять функций, которые и предстояло навесить на микроконтроллер и в дальнейшем их обосновано использовать.

Реализация

Поскольку в моих закромах нашлось аж сразу несколько контроллеров Arduino Uno R3 от RobotDyn, то было решено реализовывать проект именно на них. Точнее, всего на одном микроконтроллере. Для связи с глобальной сетью была выужена из закромов родины плата Ethernet Shield W5100 все от той же RobotDyn. В качестве датчиков для определения газов я решил использовать доступные MQ-5 и MQ-7 снова от RobotDyn. Для определения температуры и влажности я порешил использовать датчики серии DHT.

Первоначально планировалось собрать всю схему на макетной плате, затем разработать соответствующую прошивку. После провести серию испытаний, дабы убедиться, что все работает. И только тогда приступить к реализации всего в виде законченного устройства.  В процессе реализации пришлось столкнуться с некоторыми проблемами. Собственно, о них ниже.

Выявленные проблемы

В любом проекте по мере его реализации на поверхность всплывают разнообразные сложности. Ведь проект априори — деятельность по созданию чего-то нового и уникально, а где присутствует уникальность, там возникают и сложности. Не исключением стал и проект по реализации счетчика импульсов газового счетчика с отправкой их в интернет.

DHT датчики

Первое, с чем я столкнулся, так это с тем, что выбранные датчики DHT11, мягко говоря, не совсем подходят для реализации устройства. Во-первых, точность показаний датчиков желает оставлять много лучшего. Два датчика, лежащие на столе в сантиметре друг от друга и подключенные к одному контроллеру, выдают показания, отличающиеся градусов на пять. Это уже никуда не годится. Про влажность я вообще промолчу, поскольку ее измерить труднее, нежели температуру. Во-вторых, диапазон измеримых температур и влажности у DHT11 не позволяет их вообще использовать в моем проекте. Ведь в наших широтах отрицательные температуры в зимний, а иногда и в летний, период не редкость. А DHT11 имеет нижнюю границу измерений всего 0 градусов Цельсия. Что явно не подходит.

После осознания того, что датчики температуры и влажности нужно использовать другие, я вынес устное предупреждение себе самому и закупился датчиками DHT22 опять от RobotDyn. DHT22 выглядят солиднее, крупнее, точность у них выше, а температурный режим от -40 и до +125 градусов Цельсия. Что вполне в рамках предметной области.

MQ датчики

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

Один из ранних вариантов схемы с датчиками MQ

Один из ранних вариантов схемы с датчиками MQ

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

Один из последних вариантов схемы с датчиками MQ и внешним питанием

Один из последних вариантов схемы с датчиками MQ и внешним питанием

MQ датчики не только жрут амперы половниками, но и показывают просто концентрацию какого-то углерода в воздухе. По сути, они не измеряют заявленные газы, а просто меряют содержание углерода, в любом своем соединении, в воздухе. И все. Отсюда их нулевая ценность. Представьте, войдет в котельную хозяин принявший грамм 200 на грудь, а система сработает как на утечку природного газа.

Мало пинов

На реализацию всех функций по сигнализации светодиодами мне нужно соответствующее количество свободных портов для управления светодиодами. В теории, Arduino Uno оснащен достаточным количеством портов, чтобы обеспечить все запланированные светодиоды достойным управлением. А вот на практике свободных портов остается всего-то несколько штук.

Дело в том, что установленный Ethernet Shield использует 10, 11, 12, 13 и 4 цифровые пины для обеспечения собственных нужд. И подключить что-то в них — верный способ нарушить работу самой платы. Оставшегося количества цифровых пинов как раз хватает на подключение трех светодиодов, двух цифровых датчиков и счетчика импульсов.

Остальные же функции по сигнализации, пришлось реализовать, дублируя функции на тех же трех светодиодах. Так, в нормальном режиме зеленый светодиод мигает, а если отправляются данные на сервер ThingSpeak, то он горит постоянно. Желтый светодиод вспыхивает в момент получения события об импульсе. А красный мигает при возникновении предупреждений, например, возникла ошибка при чтении значений с датчика, и горит постоянно при возникновении серьезной ошибки, требующей реакции человека.

Засыпание Ethernet Shield и недостаток питания

Но сюрпризы с Ethernet Shield не ограничиваются лишь прочно занятыми пинами. Плата наотрез отказалась запускать при питании отличном от USB. Возможно, что дело в слишком хилом линейном стабилизаторе на Arduino, а может быть, тут виновато слишком большое потребление самой платы. Но факт остается фактом: хочешь использовать Ethernet, питай все от USB.

Но проблема с питанием не последняя в череде испытаний, связанных с Ethernet Shield. По непонятным и неконтролируемым причинам теряется связь с внешним миром по Ethernet. Плата продолжает работать, но пакеты более не посылаются и не принимаются. Похоже, что некий хитроумный алгоритм, зашитый где-то внутри стандартных библиотек, решает, что если никто ничего никуда не отправляет, то давайте просто отключим соединение с интернет и сэкономим немного энергии.

Web-сервер на Arduino

Web-сервер на Arduino

В результате решения возникшего затруднения, когда плата вроде работает нормально, а потом через час только и мигает светодиодами, к требуемым функциям добавилось нечто новое. Теперь моя плата отсылает показания на сервер гораздо чаще, интервал составляет около десяти минут. А заодно реализован простейший веб-сервер, отображающий основные сведения: температуры, влажности, количество импульсов счетчика, результат последней отправки сведений на сервер. В целом — помогло, Ethernet перестал засыпать летаргическим сном.

Дребезг контактов

Получать импульсы от механического датчика цифровым входом — то еще удовольствие. Новички в электронике просто не в курсе про то, что такое переходной процесс. Он возникает, когда контакты замыкаются или размыкаются. Вроде бы контакт есть, а вроде бы его и нет. Этот дребезг контактов нужно каким-то чудесным образом устранить, иначе микроконтроллер насчитает импульсов раз в сто больше, чем их было в реальности.

Избавиться от дребезга можно двумя способами: аппаратным и программным. Принцип действия обоих способов идентичен. Логика должна отсеивать быстрые изменения уровня сигнала и срабатывать только тогда, когда сигнал становится постоянным на протяжении определенного времени. Если покопаться в интернет, то можно встретить не один способ устранения дребезга при помощи аппаратных средств. Они начинаются от совсем простых схем на конденсаторе с диодом и заканчиваются специализированными микросхемами. Но реализовывать отдельную схему только для устранения дребезга контактов не входило в мои планы, тем более что настройка таких схем может отнимать порядочно ресурсов.

Поэтому в качестве решения был выбран программный способ устранения дребезга контактов, благо он отработан до самых мелочей и доступны различные реализации. Мой выбор пал на популярную библиотеку Bounce2. Хотя можно было использовать и альтернативы либо реализовать функцию самостоятельно, поскольку все библиотеку по устранению дребезга построены на одном и том же принципе. Используется счетчик, который обнуляются при каждом полученном импульсе и накапливает значения если импульс постоянен. При превышении определенного порога, устанавливается соответствующий флаг.

Длительное выполнение отправки сведений на сервера ThingSpeak

Отправлять сведения в неведомые дали занятие неблагодарное. Устройство со счетчиком установлено где-то в глухомани, а сервера окопались на другом конце света. И поди ж ты, между ними существует коммутируемая связь. В среднем, процедура отправки сведений на сервер ThingSpeak у Arduino Uno R3 занимает около 2-х секунд. И все вроде бы ничего, но за это время может поступить импульс от счетчика импульсов. А поскольку Arduino не многопоточная система, то этот импульс будет потерян.

Единственным способом как в экосистеме Arduino можно справиться с проблемой потери импульса — использование прерываний. В Arduino Uno для прерываний доступно всего 2 пина с номерами 2 и 3, на которых живут, соответственно 0 и 1 прерывания. Прерывания срабатывают на изменение уровня сигнала на соответствующем пине, на его повышение или понижение. Казалось бы, все просто. Ан нет. В экосистеме Arduino обработчики прерывания очень ограничены в действиях. Даже переменные они могут изменять только с модификатором volatile. Напомню, что у нас, в качестве устранителя дребезга контактов применяется библиотека Bounce2, методы которой и должны вызываться для обработки по прерыванию.

Нетрудно догадаться, что любая попытка вызова метода Bounce2 из обработчика прерывания приводит к полноценному провалу по причинам, описанным выше. Поэтому пришлось применить другой трюк. Опасный код «оборачивается» в прерывание:

attachInterrupt(digitalPinToInterrupt(GASMeterPin), inter, FALLING); // устанавливаем прерывание
// какой-то длительно выполняемый код
detachInterrupt(digitalPinToInterrupt(GASMeterPin)); // освобождаем прерывание
if (interruptImpulse) { // если прерывание сработало то вызываем обработчик импульсов
   interruptImpulse = false;
   addGASImpulse(F("(i)"));
}

А сам обработчик прерывания выглядит следующим образом:

void inter() {
   interruptImpulse = true;
}

Не забываем, что булева переменная interruptImpulse должна быть объявлена как volatile.

Таким нехитрым образом мы ловим один импульс, если он ненароком прибудет во время отправки данных на ThingSpeak.

Кстати, проблема с длительным выполнением присуща не только участкам кода для отправки данных в интернет. Есть куда более серьезные как с точки обнаружения, так и с точки устранения, проблемы. Например, получение показаний с датчиков DHT. Как пишет сам разработчик датчиков DHT — устройства старые, опрос одного датчика может занимать аж две секунды, а то и больше. Особенность датчиков DHT в том, что они обмениваются с микроконтроллером импульсами. И микроконтроллер должен все эти импульсы поймать и пересчитать. А для этого работу кода никто не должен прерывать. Но с DHT нам предстоит разобраться немного позже, во второй части повествования.

Иногда ThingSpeak возвращает нули

И таки да. Все иногда работает не так, как надо. При запуске устройства оно скачивает последнее значение счетчика импульсов с ThingSpeak. Стандартная библиотека от ThingSpeak подразумевает возврат кодов ошибок при отправке или получении данных, кстати, вот они:

OK_SUCCESS 200 // Все ОК
ERR_BADAPIKEY 400 // Неверный API-ключ или же неверный адрес сервера ThingSpeak
error_auth_required 401 // Требуется аутентификация
ERR_BADURL 404 // Неверный API-ключ или же неверный адрес сервера ThingSpeak
error_method_invalid 405 // Метод HTTP в запросе неприемлем
error_request_too_large 413 // Запрос слишком длинный
error_no_action 421 // Сервер попытался выполнить ваш запрос, но не смог
error_too_many_requests 429 // Слишком частые запросы к серверу
ERR_OUT_OF_RANGE -101 // Значение вне диапазона или же строка слишком длинная (>255 байт)
ERR_INVALID_FIELD_NUM -201 // Неверный номер поля
ERR_SETFIELD_NOT_CALLED -210 // Попытка вызова writeFields() без предварительного вызова setField()
ERR_CONNECT_FAILED -301 // Соединение с сервером ThingSpeak установить не удалось
ERR_UNEXPECTED_FAIL -302 // Неожиданная ошибка при записи на сервер ThingSpeak
ERR_BAD_RESPONSE -303 // Кривой ответ со стороны ThingSpeak
ERR_TIMEOUT -304 // Превышен порог ожидания ответа сервера
ERR_NOT_INSERTED -401 // Отправленные данные не были записаны, скорее всего, превышен порог на запись чаще одного раза в 15 секунд

Как видно из статусов ошибок если что-то идет не так, то, видимо, ошибку можно поймать и обработать. Но во время тестирования я столкнулся с тем, что даже если возвращается код 200, это еще не означает, что мы получаем искомый результат. Благо что при чтении возвращается не случайное число, а ноль, поэтому его достаточно легко нивелировать, периодически считывая искомое поле до тех пор, пока не будет получен настоящий результат:

while (garageGASMeter == 0) {
   garageGASMeter = ThingSpeak.readLongField(garageChannelNumber, garageGASMeterField,    garageReadAPIKey);
   delay(1000);
}

Мало памяти

В контроллере Arduino Uno R3 действительно мало памяти. Подключив все необходимые библиотеки и скомпилировав свой скетч, я получил процент загрузки на уровне 98%. Другими словами, одна из задач — загрузить устройство по полной — успешно выполнена. Но ведь с такой загрузкой нужно как-то работать.

Результат компиляции v3

Результат компиляции v3

Я проанализировал подключенные библиотеки, они оказались весьма качественно написанными и без каких-либо изъянов в плане оптимальности. По крайней мере, визуально все в порядке. Да и мой скетч не такой уж и большой. Эта статья в несколько раз больше и ничего. Причина заполнения памяти в том, что если компилировать свою программу в Arduino IDE, то будут подключаться все библиотеки, используемые Arduino, отсюда непосредственно под пользовательский код остается совсем мало места. Тем не менее для сохранения работоспособности нужно не доходить до 100%.

Наиболее эффективным способом по оптимизации памяти я могу назвать использование строковых констант, хранящихся во флеш-памяти контроллера. В Arduino IDE присутствует секретный макрос F() позволяющий конвертировать строки в константы, которые не подгружаются в оперативную память. Тем самым идет экономия памяти. По мере роста моего скетча, содержащего множество строк для вывода отладочной или служебной информации, мне периодически приходилось сокращать его объем за счет строк при помощи F().

Выглядит применение макроса примерно следующим образом:

Serial.println (F(“Hello World”));

Метод простой, но действенный. Да, библиотеки были использованы следующих версий:

Using library DHT_sensor_library at version 1.2.3
Using library ThingSpeak at version 1.1.1
Using library Bounce2 at version 2.2
Using library SPI at version 1.0
Using library Ethernet at version 1.1.2

При попытке компиляции более свежих версий скомпилированная прошивка может просто не поместиться в памяти.

Нет прерываний по таймеру нормальных, нормальных нет

В Arduino нет нормальных прерываний по таймеру, по крайней мере, они недоступны рядовому пользователю Arduino IDE. Если нужно вызвать какую-то функцию через определенные периоды времени, то приходится изгаляться.

Вся процедура изгаления состоит из периодической проверки времени в последний момент вызова функции и текущего времени. В экосистеме Arduino под временем подразумевается исключительно время от старта микроконтроллера. Вот и приходится считать микросекунды или миллисекунды. Но именно тут и порылась изюминка.

const long blinkEveryMSec = 2000;

Время в микросекундах от момента запуска контроллера хранится в переменной типа long. А у нее ограниченная размерность и в определенный момент времени происходит переполнение. В случае с миллисекундами переполнение наступает уже через каких-то 40 с лишним дней (желающие могут провести расчеты самостоятельно), а с микросекундами и того меньше.

Некоторые энтузиасты пытаются обойти ожидаемый локальный конец света путем перезагрузки контроллера и начала его жизненного цикла с нуля. Дескать при переполнении счетчика время пойдет вспять, тьма опустится на прошивку и все функции будут работать не пойми как. Действительно понять, как будет работать программа, опирающаяся на время с начала запуска, если время вдруг станет отрицательным, тяжело.

const unsigned long blinkEveryMSec = 2000;

На самом деле ларчик открывается проще, достаточно только поставить признак отсутствия знака unsigned и никаких отрицательных времен в вашей программе не наступит. Проверено на лучших любителях Arduino и НИИ «Эрмигурта».

В целом код по запуску процедур с импровизированным таймером выглядят примерно так:

void DoMeOnTime () {
   unsigned long CurrentMillis = millis();
   if (CurrentMillis - postTemp > postTempEvery) {
      postTemp = millis();
      // сюда вкорячиваем какой-то код
   }
}

В таком подходе есть и свой плюс. Функции не мешают друг другу, не прерывают исполнение и не портят общие данные. Да и выполняются они по очереди в мире и согласии. Почти настоящая не вытесняющая, кооперативная, многозадачность.

Отправка на сервер ThingSpeak с частотой не более 15 секунд

При работе с ThingSpeak следует помнить про минимальный интервал коммуникации с сервером. Он составляет 15 секунд. И если пытаться загружать свои данные чаще, то все что было отправлено в промежутке между Т и T+15сек будет сервисом отвергнуто. Дескать вам дают забесплатно, поэтому уж извольте не нагружать излишне наши ресурсы.

Бороться с этим путем заведения новых аккаунтов бессмысленно, хоть возможность по сбору показаний с нескольких каналов и не заблокирована. Куда как грамотнее просто копить данные на своей стороне и пересылать их на ThingSpeak по мере надобности, но не чаще чем раз в 15 секунд.

В моем скетче проблема с 15-секундным интервалом решена посредством флага, сигнализирующего о необходимости отправки сведений и таймера (по методике, описанной выше), который осуществляет отправку не чаще чем заданный интервал. Флаг publishIsRequired устанавливается в случае если был пойман импульс либо были обновлены тоже по таймеру, показания с датчиков. Остальное дело техники.

Схема + программа

Для самого устройства требуется минимум элементов. Три обычных, не суперярких светодиода, три резистора по 220 Ом, тактовая кнопка, клеммник, да два датчика DHT22 с обвязкой.

Схема по сборке счетчика на макетной плате. Ethernet Shield не изображен.

Схема по сборке счетчика на макетной плате. Ethernet Shield не изображен.

Для создания скетча, я использовал следующие библиотеки:

DHT.h – для получения данных с датчиков DHT, официальная библиотека от AdaFruit на GitHub.

ThingSpeak.h – для коммуникации с сервером ThingSpeak. Официальная библиотека доступна на GitHub.

Bounce2.h – популярная библиотека для устранения дребезга контактов, доступна с GitHub.

SPI.h – стандартная библиотека для коммуникации между микроконтроллерами и другими устройствами, через нее работает Ethernet Shield.

Ethernet.h – библиотека для Ethernet Shield.

Библиотеки можно установить как через git, так и через менеджер библиотек в Arduino IDE.

Для общего понимания кода скетча и всей логики, стоящей за разработкой, я поясню суть и назначение некоторых функций программы. В самом начале исходного кода, после объявления библиотек, идет объявление констант и переменных конкретного скетча. У меня они сгруппированы для облегчения понимания.

В функции setup() происходит инициализация всех первоначальных значений. В первую очередь инициализируются пины светодиодов. Первоначально все светодиоды переключаются в рабочий режим и горят до тех пор, пока не произойдет полная инициализация. Тогда они будут погашены, за исключением светодиода, обозначающего ошибку и предупреждения. Его поведение регулируется наличием или отсутствием ошибок во время инициализации.

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

pinMode(GASMeterPin, INPUT_PULLUP);

Тут же, в setup() инициализируется и подключение к ThingSpeak. Считывается количество стартов платы и последнее сохраненное значение количества импульсов. Количество стартов важно для отслеживания не только циклов отключения электричества, но и для контроля работоспособности самого устройства. Если по мониторингу видно, что плата стартует каждые несколько часов, то с ней явно что-то не так. Тем не менее правильность значения, возвращаемого с сервера ThingSpeak не так важно, поэтому его не проверяем дополнительно. А вот значение счетчика проверяется и если оно нулевое, то происходит повторный запрос значения.

В конце функции setup() производится вывод в последовательный порт всех полученных значений.

В функции loop() последовательно вызываются все функции в скетче. Но особе внимание стоит обратить на использование вызовов методов bounce2.

deBouncer.update(); // check for the impulse
if (deBouncer.rose()) {
   addGASImpulse("");
}

При помощи метода update() происходит обновление счетчиков библиотеки для подавления дребезга контактов. Предполагается, что весь скетч выполняется достаточно быстро, чтобы единичного вызова было достаточно для надежного считывания импульсов. Сами они считаются через ловлю увеличения напряжения на подключенном пине, о чем и говорит вызов метода deBouncer.rose(). Дело в том, что при нажатии кнопки или замыкании контактов в счетчике импульсов, происходит замыкание пина на землю через подтягивающий резистор, соответственно напряжение понижается. А вот при отпускании происходит увеличение напряжения. Другими словами, импульс ловится, когда контакты, кнопки или счетчика, размыкаются. Здесь следует учитывать, что бывают ситуации, когда импульсный счетчик зависает на импульсе и может находиться в таком состоянии до тех пор, пока не будет израсходована следующая порция газа.

Функция void addGASImpulse(String sVal) увеличивает внутренний счетчик импульсов, записывает соответствующее значение в поле объекта ThingSpeak и поднимает флаг необходимости отправки сведений на сервер.

Функция void errorLED() отвечает за красный светодиод. Если в системе нет никаких ошибок и предупреждений, то светодиод погашен. Если есть ошибка, то он включается и горит постоянно. А в случае предупреждений, например, ошибках в чтении показаний сенсора, светодиод начинает мигать. Время его полного мигания зависит от количества предупреждений. Чем больше предупреждений, тем дольше будет мигать. Предупреждения они же не критичны и если проблема с сенсором была сутки тому назад, то сейчас мигание уже совсем неактуально.

Функция void blinker() применяется для управления зеленым светодиодом. В нормальном состоянии он мигает с заданной частотой. А в случаях, когда происходит отправка сведений на сервер ThingSpeak он загорается и не гаснет пока отправка не будет завершена.

Отправка сведений в ThingSpeak осуществляется функцией void publisher(). Если поднят флаг publishIsRequired, то происходит отправка, а затем флаг очищается. Особенность библиотеки ThingSpeak в том, что в ней существует два способа накопления и отправки данных. В первом варианте, программа сама накапливает сведения в своих структурах, а потом эти структуры передаются на сервер. Но я выбрал другой вариант, когда данные записываются в требуемые поля через ThingSpeak.setField. Затем все накопленные поля передаются на сервер при помощи writeFields и значения очищаются.

Функция TempAndHumidity() является оберткой для функции получения значений с сенсоров. Основное ее предназначение — получение значений по расписанию. Напомню, что сенсоры семейства DHT не самые быстрые штуки на свете и не стоит их опрашивать в каждом цикле.

void EhternetWebServer() отвечает за обработку запросов к веб-серверу с текущими показаниями с сенсоров и результатов прошлых отправок. Поскольку в стандартной библиотеке Ethernet возможности по обработке http-запросов весьма примитивные, то в этой функции выводится даже заголовок ответа сервера, да и на любой запрос будет выводиться единый ответ в виде простенькой странички.

int memoryFree() — вспомогательная функция, применяется для отображения свободной памяти в микроконтроллере. Ее мало и любая утечка должна быть обнаружена еще на стадии тестирования.

void SensorsUpdate() — одна из самых загруженных функций. Тут происходит опрос сенсоров на предмет их значений. А загруженной функция является из-за вызова методов библиотеки DHT. Немного забегая вперед, я раскрою карты. В этой библиотеке происходит запрет прерываний и используется несколько вложенных циклов для ловли импульсов от сенсоров. Вследствие чего, результат работы методов часто непредсказуем, а задержка исполнения может занимать секунды. Но на плате с Arduino никаких побочных эффектов я не обнаружил. Все работает примерно так, как и должно. Поэтому никакого дополнительного кода в функции по обновлению значений с сенсоров у меня нет.

// Temperature, Humidity, CO, Metane, Gas meter impulse monitoring with Arduino v.3.0
// Copyright Vladislav Kravchenko (c) 2016
// You can use this code for free for non-commercial usage. Amendmend of code is allowed. Reference to the initial author is mandatory.
// http://kvv213.com
#include <DHT.h>
#include <ThingSpeak.h>
#include <Bounce2.h> // Documentation to Bounce 2 is here https://github.com/thomasfredericks/Bounce2/wiki
#include <SPI.h>
#include <Ethernet.h>
// Example is here https://www.arduino.cc/en/Tutorial/WebClient
// Ethernet digital pins 13,12,11,10 are used for Ethernet board
// SD pin is d4

// =================== initialize Ethernet =========================================
byte mac[] = { 0xDE, 0xAD, 0xAD, 0xEF, 0xFE, 0xED };
EthernetClient client; // All functions https://www.arduino.cc/en/Reference/Ethernet
EthernetServer server(80); // Simple web server
// =================================================================================

// =================== PINs Set-Up =========================================
const int GASMeterPin = 2; // Pin for Gas Meter connection
const int GasMeterImpulsePin = 5; // Pin for gas meter impulse

// =================== Blinkers =========================================
const int StatusBlinkerPin = 6; // Pin for Status blink
const unsigned long blinkEveryMSec = 2000; // msec for total blink cycle
const unsigned long blinkForMSec = 5; // msec for emitting light by LED
boolean isBlinking = false; // when true then LED is working
unsigned long blinkerMillis; // here we store values for switching
unsigned long blinkerMillisGAS = 0; // for temp purposes for GAS Implse LED
const unsigned long blinkForMSecGAS = 250; // msec for emitting light by GAS LED

// ========= Sensors =====================================================
const int DHT22InPin = 3; // Digital Temperature and Humidity Sensor for Inside
const int DHT22OutPin = 7; // Digital Temperature and Humidity Sensor for Outside
const unsigned long postTempEvery = 180000; // Post temperature and humidity every 3 minutes
unsigned long postTemp = 0; // here we store values for switching
float hIn;
float hOut;
float tIn;
float tOut;
#define DHTTYPE DHT22
DHT dhtIn(DHT22InPin, DHTTYPE);
DHT dhtOut(DHT22OutPin, DHTTYPE);

// Error PIN
const int ErrorPin = 4; // Pin error indicator
const unsigned long blinkEveryMSecEr = 1000; // msec for total blink cycle
const unsigned long blinkForMSecEr = 15; // msec for emitting light by LED
boolean isBlinkingEr = false; // when true then LED is working
unsigned long blinkerMillisEr; // here we store values for switching

unsigned long systemWarning = 0; // the more value is the more warnings were
const int warningsTimeOut = 600; // how long one warning will blink
boolean systemError = false; // 0 - no errors, 1 - warning, 2 - error
// ============================================================================

// ============= ThingSpeak channel set-up ====================================
unsigned long garageChannelNumber = 137962; // Garage Channel ID
const char * garageWriteAPIKey = "ххх"; // Garage API-Key
const char * garageReadAPIKey = "ххх"; // Garage Read API-Key
const int garageGASMeterField = 1; // Field for GasMeter
const int garageStartUpsField = 6; // Field for StartUps count
const int garageTempIn = 2; // Field for Temperature In
const int garageTempOut = 7; // Field for Temperature Out
const int garageHumIn = 3; // Field for Humidity In
const int garageHumOut = 8; // Field for Humidity Out
const int garageMetane = 4; // Field for Humidity In
const int garageCO = 5; // Field for Humidity Out
volatile long garageGASMeter = 0; // Field for GAS Meter storage
int LastPublishStatus = 0; // Keeps last publish status
// ============================================================================

// ==================== publish SetUp =========================================
const unsigned long publishEvery = 60000; // publish time period
unsigned long publishTemp = 0; // temp values storage for publishsing
volatile boolean publishIsRequired = false; // TRUE when we have something to publish
// ============================================================================

long totalStarts; // how much times the system has been started
Bounce deBouncer = Bounce(); // de bouncer

// ==================== for Memory Free =========================================
extern int __bss_end;
extern void *__brkval;
volatile boolean interruptImpulse = false; // Catches impulse when Sketch is busy with Internet publishing

void setup() {
  { // Status Blinker Init
    pinMode(StatusBlinkerPin, OUTPUT);
    digitalWrite(StatusBlinkerPin, HIGH); // let me know when the Setup is executed
    blinkerMillis = millis();
  }
  { // Init of Error LED
    pinMode(ErrorPin, OUTPUT);
    digitalWrite(ErrorPin, HIGH); // Is On when Setup is executing
  }
  { // GAS Meter Blinker
    pinMode(GasMeterImpulsePin, OUTPUT);
    digitalWrite(GasMeterImpulsePin, HIGH);
  }
  { // The Welcome Screen
    Serial.begin(9600);
    Serial.println(F("KVV@Dacha Garage monitoring system"));
  }
  { // Sensor initialization
    pinMode(GASMeterPin, INPUT_PULLUP); // initialize GAS meter pin
    deBouncer.attach(GASMeterPin); // Initialize DeBouncer
    deBouncer.interval(50); // ms for bounce suppress
  }
  if (Ethernet.begin(mac) == 0) {// https://www.arduino.cc/en/Reference/EthernetBegin
    Serial.println(F("Failed to configure Ethernet using DHCP"));
    systemError = true; // that means a serious error
  }
  Serial.println(F("Ethernet connection is OK"));
  dhtIn.begin(); // init DHT In sensor
  dhtOut.begin(); // init DHT Out sensor
  //delay(1000); // give the Ethernet shield a second to initialize:
  { // Web-server init
    server.begin(); // Starts Web-Server
    Serial.print(F("Web-server is at "));
    Serial.println(Ethernet.localIP());
  }
  if (ThingSpeak.begin(client)) { // initialize ThingsSpeak
    totalStarts = ThingSpeak.readLongField(garageChannelNumber, garageStartUpsField, garageReadAPIKey);
    totalStarts++;
    { // Check that ThingSPeak returns not ZERO value. It should be updated to the latest value before the first start
      garageGASMeter = 0;
      while (garageGASMeter == 0) {
        garageGASMeter = ThingSpeak.readLongField(garageChannelNumber, garageGASMeterField, garageReadAPIKey);
        delay(1000);
      }
    }
    ThingSpeak.setField(garageStartUpsField, totalStarts); // increase value of Arduino starts
    int resultCode = ThingSpeak.writeFields(garageChannelNumber, garageWriteAPIKey);
    if (resultCode == 200)
    {
      Serial.print(F("Startups to date: "));
      Serial.println(totalStarts);
      Serial.print(F("GAS Meter to date: "));
      Serial.println(garageGASMeter);
      Serial.print(F("Latest message is: "));
      Serial.println(resultCode);
    }
    else
    {
      Serial.print(F("Error reading message.  Status was: "));
      Serial.println(resultCode);
      systemWarning = systemWarning + warningsTimeOut; // that means a not very serious error
    }
  } else {
    Serial.println(F("Something went wrong with ThingSpeak initializing"));
    systemError = true; // that means a serious error
  }
  delay(100);
  digitalWrite(StatusBlinkerPin, LOW); // let me know when the Setup is finished to be executed
  digitalWrite(GasMeterImpulsePin, LOW);
}

void loop() {
  errorLED(); // working with ERROR LED
  deBouncer.update(); // check for the impulse
  if (deBouncer.rose()) {
    addGASImpulse("");
  }  
  delay(10);
  blinker(); // call for status LED blink
  EhternetWebServer();
  TempAndHumidity(); // update Temperature, Humidity, Methane and CO
  publisher(); // Update ThingSpeak
}
void inter() {
  interruptImpulse = true;
}
void addGASImpulse(String sVal){
    digitalWrite(GasMeterImpulsePin, HIGH);
    garageGASMeter++;
    publishIsRequired = true;
    Serial.print(F("Pressed "));
    Serial.print(sVal);
    Serial.println(garageGASMeter);
    ThingSpeak.setField(garageGASMeterField, garageGASMeter);
}
void errorLED() { // blinks with error_led
  if (systemError == false) { // No Errors shutdown Error LED
    digitalWrite(ErrorPin, LOW);
  }
  if (systemError == true) { // Critical Error constant LED eimmiting
    digitalWrite(ErrorPin, HIGH);
  }
  if (systemWarning > 0) { // There is a Warning - let's blink!
    unsigned long CurrentMillisEr = millis();
    if (!isBlinkingEr && CurrentMillisEr - blinkerMillisEr > blinkEveryMSecEr) {
      isBlinkingEr = true;
      blinkerMillisEr = millis();
      digitalWrite(ErrorPin, HIGH);
    }
    if (isBlinkingEr && blinkerMillisEr + blinkForMSecEr < CurrentMillisEr) {
      isBlinkingEr = false;
      blinkerMillisEr = millis();
      digitalWrite(ErrorPin, LOW);
      if (systemWarning > 0) {
        systemWarning--;
      }
    }
  }
}
void blinker() { // Blinks with status LED (short blinks - system is working, long blink updating values to ThingSpeak
  unsigned long CurrentMillis = millis();
  if (!isBlinking && CurrentMillis - blinkerMillis > blinkEveryMSec) {
    isBlinking = true;
    blinkerMillis = millis();
    digitalWrite(StatusBlinkerPin, HIGH);
  }
  if (isBlinking && blinkerMillis + blinkForMSec < CurrentMillis) {
    isBlinking = false;
    blinkerMillis = millis();
    digitalWrite(StatusBlinkerPin, LOW);
  }
  { // Turn Off GAS Impulse LED
    if (blinkerMillisGAS + blinkForMSecGAS < CurrentMillis) {
      digitalWrite(GasMeterImpulsePin, LOW);
    }
  }
}
void publisher() { // uploads data to ThingSpeak
  unsigned long CurrentMillis = millis();
  if (CurrentMillis - publishTemp > publishEvery) {
    publishTemp = millis();
    if (publishIsRequired) {
      attachInterrupt(digitalPinToInterrupt(GASMeterPin), inter, FALLING); //use interrupt in order not to oversee an impulse
      digitalWrite(StatusBlinkerPin, HIGH); // Turn On Status LED
      LastPublishStatus = ThingSpeak.writeFields(garageChannelNumber, garageWriteAPIKey);
      if (LastPublishStatus == 200) {
        Serial.print(F("ThingSpeak updated. Gas value: "));
        Serial.println(garageGASMeter);
        publishIsRequired = false; // Good job is done!
      } else {
        Serial.print(F("ThingSpeak failed! Responce code: "));
        Serial.println(LastPublishStatus);
        systemWarning = systemWarning + warningsTimeOut; // that means an error and we should repeat
      }
      detachInterrupt(digitalPinToInterrupt(GASMeterPin)); // stop to use interrupts      
      if (interruptImpulse) { // Try to catch GAS meter impulse via interrupt
        interruptImpulse = false;
        addGASImpulse(F("(i)"));
      }
    }
  }
}
void TempAndHumidity() { // Post Temp And Humidity
  unsigned long CurrentMillis = millis();
  if (CurrentMillis - postTemp > postTempEvery) {
    postTemp = millis();
    SensorsUpdate();
    publishIsRequired = true;
    ThingSpeak.setField(garageTempIn, tIn);
    ThingSpeak.setField(garageTempOut, tOut);
    ThingSpeak.setField(garageHumIn, hIn);
    ThingSpeak.setField(garageHumOut, hOut);
  }
}
void EhternetWebServer() {
  EthernetClient client = server.available();
  if (client) {
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        if (c == '\n' && currentLineIsBlank) {
          // send a standard http response header
          client.println(F("HTTP/1.1 200 OK"));
          client.println(F("Content-Type: text/html"));
          client.println(F("Connection: close"));  // the connection will be closed after completion of the response
          client.println();
          client.println(F("<!DOCTYPE HTML>"));
          client.println(F("<html>"));
          // output meaning values
          client.println(F("<h1>Welcome to Arduino Web-server @Garage</h1><br/><u>Current values:</u><br />Startups to date: "));
          client.println(totalStarts);
          client.print(F("<br/>GAS Meter to date: "));
          client.println(garageGASMeter);
          client.print(F("<br/>Temperature in/out: "));
          client.print(tIn);
          client.print(F("/"));
          client.print(tOut);
          client.print(F("<br/>Humidity in/out: "));
          client.print(hIn);
          client.print(F("/"));
          client.print(hOut);
          client.print(F("<br/><br/>Latest data upload message is: "));
          client.println(LastPublishStatus);
          client.print(F("<br/>RAM free: "));
          client.println(memoryFree());
          client.println(F("</html>"));
          break;
        }
        if (c == '\n') {
          // you're starting a new line
          currentLineIsBlank = true;
        } else if (c != '\r') {
          // you've gotten a character on the current line
          currentLineIsBlank = false;
        }
      }
    }
    // give the web browser time to receive the data
    delay(1);
    // close the connection:
    client.stop();
  }
}
int memoryFree() {
  int freeValue;
  if ((int)__brkval == 0)
    freeValue = ((int)&freeValue) - ((int)&__bss_end);
  else
    freeValue = ((int)&freeValue) - ((int)__brkval);
  return freeValue;
}
void SensorsUpdate() {
  // Reading temperature or humidity takes about 250 milliseconds!
  // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
  hIn = dhtIn.readHumidity();
  hOut = dhtOut.readHumidity();
  // Read temperature as Celsius (the default)
  tIn = dhtIn.readTemperature();
  tOut = dhtOut.readTemperature();

  // Check if any reads failed and exit early (to try again).
  if (isnan(hIn) || isnan(tIn)) {
    Serial.println(F("Failed to read from DHT In sensor!"));
    //return;
  }
  if (isnan(hOut) || isnan(tOut)) {
    Serial.println(F("Failed to read from DHT Out sensor!"));
    //return;
  }
  { // print some read values
    Serial.print(F("Humidity in/out: "));
    Serial.print(hIn);
    Serial.print("/");
    Serial.print(hOut);
    Serial.println(" %\t");
    Serial.print(F("Temperature in/out: "));
    Serial.print(tIn);
    Serial.print("/");
    Serial.print(tOut);
    Serial.println(F(" *C "));
  }
}

Ну и собственно на этом все по самому скетчу.

Выводы

В принципе, все требования, за исключением анализатора газа, были реализованы на основе выбранной аппаратной базы. Импульсы ловятся, сведения отправляются, светодиоды мигают. Но захотелось реализовать нечто больше. Внезапно стало очевидно, что помимо температуры и влажности хорошо бы мониторить еще и атмосферное давление. А подключение по витой паре желательно заменить на Wi-Fi. Да и прочие функции, например, веб-сервер не мешало бы улучшить, добавить более внятную информацию, выводить больше сведений. И для всего этого хорошо бы заполучить контроллер помощнее. И именно тут было принято решение о реализации следующей версии системы на контроллере с чипом ESP8266. Там и Wi-Fi есть встроенный, да и памяти не просто море, а целый океан. Но об этом я расскажу во второй части.

Ознакомиться со всем циклом статей можно по следующим ссылкам:
Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 1. Используем Arduino Uno R3.
Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 2. Используем ESP8266.
Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 3. Собираем все вместе.
Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 4. Обрабатываем значения.
Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 5. Избавляемся от сенсоров DHT.



Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 1. Используем Arduino Uno R3.: 4 комментария

  1. Уведомление: Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 5. Избавляемся от сенсоров DHT. | Мно

  2. Уведомление: Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 4. Обрабатываем значения. | Многоб

  3. Уведомление: Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 3. Собираем все вместе. | Многобукф

  4. Уведомление: Измеряем температуру, влажность и отслеживаем показания газового счетчика с использованием ThingSpeak. Часть 2. Используем ESP8266. | Многобукфф

Добавить комментарий