Погодная станция с Ethernet и планшетом в качестве устройства отображения

в 8:15, , рубрики: Без рубрики

Введение

Я – пользователь бытовых погодных станций со стажем, и в этом скрыта двойная катастрофа. Во-первых, я уже настолько привык к тому, что погода внутри и вне дома мне известна, что отсутствие этой информации вводит меня в состояние когнитивного диссонанса. Во-вторых, с погодными станциями мне хронически не везет. Две из них сломались и, как в таких случаях говорят про всяческие индикаторные приборы, стали «показывать погоду», только с точностью до наоборот, показывали они что угодно, только не погоду. Из Штатов привез с собой третью, которая прослужила мне верой и правдой целый год, и я начал было потихоньку расслабляться, но тут у случайно забежавших (и давших нам с женой возможность временно возложить на их плечи заботу о сыне и выбежать из дома) родителей случился приступ принужденной заботы, в свою очередь, вызвавший острое желание помыть окна. Результат тщательного натирания окон – не только сверкающие окна, но и прыгнувший вниз датчик погодной станции. Этаж второй, так что датчик разбился бы вряд ли, но, как известно, непосредственно под окнами каждой многоэтажки есть невидимая пространственно-временная сингулярность. Не знаю, можно ли этот феномен описать в рамках Стандартной Модели, но то, что в основе сингулярности лежит принцип «что упало, то пропало», сомнений нет. Так что датчик я не нашел.
Естественно, в полный рост встала нужда менять девайс, но мысль о том, что ни одна из «бывших» не продержалась у меня дома больше года, настораживала. И тогда в голову забрела идея «а не сделать ли самому?».

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

Изначально идея состояла в создании некоторого количества датчиков температуры и влажности, размещенных в разных частях жилища и на внешних его стенах, и центрального юнита. Датчики должны были снимать данные и передавать их на центральный юнит, который, в свою очередь, должен был их визуализировать. В качестве побочной функциональности рассматривалась возможность оправки данных по Ethernet на имеющийся сервер для целей сборки статистики (люблю посмотреть «как оно было») и последующего трансфера на обитающие дома гаджеты вроде айпэдов, айфонов, андроидов и прочей электронной нечисти.

Очень быстро (а именно на стадии изучения возможностей не очень дорогих микроконтроллеров и имеющейся в продаже периферии) идея визуализации на центральном юните отмерла, так что вариант отправки на сервер с последующей визуализацией на подручных устройствах приобрела статус головной.
В качестве платформы для реализации идеи была выбрана Arduino. Причины: развитая инфраструктура, куча примеров, куча компонент в продаже, цена (если говорить о зеркальных китайских клонах, а не о genuine Arduino), доступность (благо на Ebay их можно найти во множестве и на любой вкус). В принципе, все то же самое можно было реализовать и на голых контроллерах, но, учитывая факт, что я в технологию только входил, увеличивать и так имеющийся порог входа не хотелось. Единственное, хоть погодный датчик и не эстетическое устройство, но вешать на стену гроб с линейным размером 20 см не хотелось, поэтому в качестве основы был выбран Arduino Pro Mini, а не какой-нибудь внушительных размеров Uno.

Итак, концептуально система состоит из четырех частей:

  • Погодные датчики. Задача – мерить погоду и посылать данные по радиоканалу на центральный юнит
  • Центральный юнит. Задача – принимать данные о погоде, оборачивать их в HTTP и постить на сервер
  • Сервер. Задача – принимать данные от центрального юнита, сохранять их в БД и передавать в виде, пригодном для визуализации на всяких домашних гаджетах.
  • Устройства визуализации. Задача – показывать погоду пользователю

Протокол обмена

Прежде чем переходить к описанию составляющих системы следует осуществить краткий экскурс в протокол и принцип обмена данными в радиоэфире. Радиообмен сделан на основе библиотеки VirtualWire, которая уже имеет средства «правильной» передачи, вроде манчестерского кодирования и проверки контрольных сумм. Поверх этой библиотеки был реализован микропротокол с фиксированной структурой пакета. Каждый пакет состоит из трех полей – адреса передающего устройства (uint16), типа датчика (int16) и данных (float). Никакой дополнительной проверки корректности пришедших данных, кроме проверки длины пакета не производится, поскольку дополнительные средства защиты реализованы на стороне сервера, куда в конечном итоге будут попадать данные. Адрес передающего устройства генерируется случайно при включении устройства, если он не был сгенерирован ранее, и записывается в EEPROM. Каждое из устройств имеет право передавать сколь угодно много показаний датчиков разных типов. Так, каждый погодный датчик передает по два пакета – пакеты с показаниями температуры и показаниями влажности.
Таким образом, организована радиосеть с топологией «общая шина», но без каких-либо высокоуровневых возможностей вроде детекции коллизий и гарантий доставки. Если два устройства пытаются передать данные одновременно, данные теряются в результате коллизии.
Реализация микропротокола была оформлена в отдельную библиотеку, содержащую два основных метода – send и receive. Все остальные методы – сервисные, необходимые, например, для генерации адреса передающего устройства.

Исходный код библиотеки

// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// Created by Andrey Filimonov 2014

#ifndef WIRELESSSENSORPIPE_H
#define WIRELESSSENSORPIPE_H
#if ARDUINO >= 100
 #include "Arduino.h"
#else
 #include "WProgram.h"
#endif

class WirelessSensorPipe
{
  public:
    enum SensorType
    {
      TEMPERATURE = 0,
      HUMIDITY,
      PRESSURE,
      WATERFLOW,
      HEATERSETPOINT,
      HEATERFLAMEENABLED,
      HEATERRWTEMPERATURE,
      HEATERTARGETROOMTEMPERATURE
    };
    struct Packet
    {
      int16_t id;
      SensorType type;
      float value;
    };

  private:
    Packet data;
    int16_t _id;

  public:
    void begin(uint16_t transmitPin, uint16_t receivePin, uint16_t pttPin = 10, uint16_t eepromAddress = 0);
    void send(SensorType type, float value);
    bool receive(Packet& packet, uint16_t timeout = 30000);
    int16_t id() {return _id;};
    
  private:
    void EEPROMWriteInt(int p_address, int16_t p_value);
    int16_t EEPROMReadInt(int p_address);
};

#endif

// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// Created by Andrey Filimonov 2014

#include "WirelessSensorPipe.h"

#include "VirtualWire.h"
#include "EEPROM.h"

//#define DEBUG

void WirelessSensorPipe::EEPROMWriteInt(int p_address, int16_t p_value)
{
  byte lowByte = ((p_value >> 0) & 0xFF);
  byte highByte = ((p_value >> 8) & 0xFF);

  EEPROM.write(p_address, lowByte);
  EEPROM.write(p_address + 1, highByte);
}

int16_t WirelessSensorPipe::EEPROMReadInt(int p_address)
{
  byte lowByte = EEPROM.read(p_address);
  byte highByte = EEPROM.read(p_address + 1);

  return ((lowByte << 0) & 0xFF) + ((highByte << 8) & 0xFF00);
}

void WirelessSensorPipe::begin(uint16_t transmitPin, uint16_t receivePin, uint16_t pttPin, uint16_t eepromAddress)
{
  // Initialise the IO and ISR
  vw_set_rx_pin(receivePin);
  vw_set_tx_pin(transmitPin); 
  vw_set_ptt_pin(pttPin);
  vw_setup(256);	 // Bits per sec
  vw_rx_start();       // Start the receiver PLL running
  
  _id = EEPROMReadInt(eepromAddress);
  if (_id <= 10000)
  {
    randomSeed(analogRead(0));
    _id = random(10000, 60000);
    EEPROMWriteInt(eepromAddress, _id);
  }
}

void WirelessSensorPipe::send(SensorType type, float value)
{
  data.id = _id;
  data.type = type;
  data.value = value;  
  vw_send((uint8_t *)&data, sizeof(Packet));
  vw_wait_tx(); // Wait until the whole message is gone
}

bool WirelessSensorPipe::receive(Packet& packet, uint16_t timeout)
{
  bool result = false;
  uint8_t message_length = sizeof(Packet);
  if (vw_wait_rx_max(timeout) && 
    vw_get_message((uint8_t*)&data, &message_length) && 
    message_length == sizeof(Packet))
  {
    packet.id = data.id;
    packet.type = data.type;
    packet.value = data.value;
    result = true;
  }
  return result;
}

Погодные датчики

Казалось, что сделать погодный датчик – дело простое. На имеющийся МК (напомню это ATMega328 в составе Arduino Pro Mini) цепляем датчик температуры и влажности (в проекте это был DHT11), цепляем передатчик OOK на 433МГц, делаем софтверный насос, который выкачивает данные из датчика, закачивает в передатчик, и все, дело сделано. Как подключить DHT11 к Arduino описывать не буду, материалов такого рода полно, в том числе и на Хабре (http://habrahabr.ru/post/171525/, http://habrahabr.ru/post/184966/). Подключить OOK передатчик на 433МГц – тоже не проблема (http://habrahabr.ru/post/210830/). Для получения данных используется библиотека DHT от Adafruit (набор библиотек можно найти тут: https://github.com/adafruit). Для организации связи используется описанный микропротокол.

Все было бы просто, если бы не тяга к перфекционизму. Дело в том, что в мозге укоренилось убеждение, что датчик должен работать от батареек и должен работать долго. И вот тут крылась засада, потому что Arduino в его исходном состоянии, если не предпринять никаких мер, потребляет около 50mA. Оптимистичная емкость типичной батарейки AAA – 1200mAh. С таким аппетитом комплекта батареек хватит на 24 часа. Поскольку идея раз в сутки менять батарейки во всех датчиках у меня восторга не вызывала, я пустился во все тяжкие, изучая режимы эноргосберегайки контроллеров от Atmel. Кроме софтверных трюков, нужно было что-то делать с периферией, а именно с DHT11 и передатчиком. Чтобы они постоянно не высасывали батарейку, было решено подключить их +5V не к шине питания, а к отдельной ножке Arduino. Это позволило бы включать периферию только тогда, когда она нужна. А, поскольку, передавать погодные данные чаще раза в 5 минут не имеет смысла, то и периферия нужна раз в 5 минут на несколько секунд. По спецификациям ATmega328 способен скармливать 40mA через свои пины, с другой стороны, потребление передатчика – 30ма, потребление DHT11 много меньше, плюс они одновременно никогда не работают, сначала считываются данные с датчика, потом они передаются. Так что 40mA должно быть более чем достаточно. Подытожим, в приборе есть три потребителя:

  1. Периферия (виртуально с ней разобрались, запитав от отдельной ноги)
  2. Сам микроконтроллер (тут нужно разбираться с энергосбережением)
  3. Ненужная периферия на плате Arduino (например, светодиод, индицирующий наличие питания)

Светодиод и преобразователь напряжения из +6.5V…12V в 5V, присутствующий на плате Arduino и необходимый, если вы хотите питать его чем угодно, лишь бы это что угодно было не больше 12V, были ампутированы сразу, поскольку питание датчика предполагалось в виде трех батареек AAA общим напряжением 3.7V…5.1V (в зависимости от степени свежести), так что понижающий преобразователь ни к чему. Светодиод мне тоже ни к чему, его внутри корпуса все равно никто не увидит. Ура, хирургия привела к снижению аппетита почти в два раза – до 28mA. Но это все равно много. Поскольку к моменту измерения питание периферии уже осуществлялось от ноги МК и было на тот момент отключено, был сделан вывод, что 28mA – это аппетит самого контроллера. Глушить его можно только софтверно. Было прочитано множество инструкций и руководств. Больше всего знаний содержит вот это. Пропуская мытарства с энергосбережением, скажу, что больше всего помогли включение энергосберегающего режима SLEEP_MODE_PWR_DOWN с выходом из него по срабатыванию watchdog-таймера, отключение аналого-цифрового преобразователя и отключение детекции режима работы с недопустимо низким напряжением питания (так называемый brown-out режим). Если обойтись только первыми двумя пунктами из моего списка по превращению энергетического толстяка в анарексика, то потребление составит около 20uA. Это в тысячу раз меньше исходных 28mA, и в принципе, на этом можно было бы остановиться, но даташит к ATmega328 говорит, что когда микроконтроллер ничем не занят, он может потреблять меньше 1uA!
Внутри МК разные устройства по-разному реагируют на падение питающего напряжения. Так что по мере снижения питающего напряжения, вы можете получить ситуацию, когда основная часть кристалла работает, а некоторые устройства – нет, либо работают нестабильно, либо не так как надо. Чтобы избежать такой железячной неконсистентности МК может обнаруживать падение питающего напряжения ниже критического уровня и отключаться полностью. Однако, эта возможность, как выяснилось, требует довольно значительной мощности. Отключение brown-out привело к тому, что мой китайский мультиметр DT838 в режиме измерения микроамперных токов показал, что ничего не чувствует. Вот оно! Вряд ли можно сказать, что ток стал меньше 1uA, но он точно стал меньше порога чувствительности микроамперметра, который, в свою очередь, заведомо меньше 20uA. Поскольку тренироваться дальше не позволяла точность измерений, было решено счесть результат удовлетворительным и закончить изыскания по энергопотреблению.
Итак, датчик был собран, вовнутрь зашит код, реализующий тот самый насос, включающийся раз в 5 минут и уходящий в глубокий сон в остальное время. На макетной доске на скорую руку был собран приемник для передаваемых данных. Все было запитано и… Обычно в таких местах после троеточия пишут какой-нибудь неожиданный эпик фэйл, но нет, все пошло штатно, тестовый приемник стал принимать данные и данные выглядели совершенно адекватно: температура – 25 градусов, влажность – 40%. Ура, товарищи! Дело сделано. Для интереса было решено поместить устройства в условия, приближенные к полевым, а именно увеличить расстояние между передатчиком и приемником с 10см до 5м. И вот тут подкрался эпик фэйл. В условиях прямой видимости, на расстоянии 5м приемник не регистрировал ничего. Я был готов к такому повороту событий и думал, что знал что происходит. Дело в том, что ни приемник, ни передатчик на тот момент не обладали антенной. По всем правилам тут же была отмеряна и отрезана пара четвертьволновых антенн (напомню, что для 433MHz это примерно 17.5см провода). Проводники были запаяны на место антенн и чудо, передача пошла! Следующий шаг – устранить прямую видимость, поставив между приемником и передатчиком стену из газосиликата толщиной 375мм. И опять беда, передача то есть, то ее нет. Танцы с местоположением устройств, антенн, их формой, внутренностями (многожильный провод, одножильный, в разных изоляциях (зачем?)) ни к чему не привели. Снижение скорости передачи до смешных 256 бит/с не сильно улучшило ситуацию. В попытке заклеймить антенны и мой недостаток знаний в области радиофизики привели к привлечению к процессу трех человек с радиофизическим образованием, один из которых – кандидат физмат наук. Все хором заявили что-то вроде «антенна тут ни причем, эти передатчики при условии правильного кодирования сигнала работают с любой фигней, которая прицеплена в качестве антенны, ты, наверное, сделал что-то особенное, чтобы они не работали в пределах 10 метров». Прожевав сарказм и отпарировав, что кодирование правильное, поскольку VirtualWire использует манчестерский код с дополнительными проверками поверх него, я стал думать, что мог сделать не так. Если отбросить все кажущиеся очевидными, но все же невероятные предположения, что мне попались бракованные передатчики или приемники (у меня их было пять комплектов, я их перепробовал, результат один и тот же), оставалась только одна версия – я недопитываю передатчик. В экспериментальных целях перенес питание передатчика с ноги МК на шину +5V и вуаля, на приемнике есть данные. Причем они есть вне зависимости от того, в каком положении антенна, из чего она сделана и где в рамках дома расположены приемник и передатчик. Единственные обстоятельства, в которых мне удалось заставить передачу прерваться, это накрыть передатчик железным колпаком (но это уже из серии «а-а-а-а, сказали суровые сибирские мужики»). К тому же, пораскинув мозгами 10 секунд (почему раньше не сделал?) понял, что OOK-передатчик ничего не кушает, когда ничего не передает, поэтому беречь от него батарейку каким-либо “внешним” способом смысла не имеет. Время для версии, почему передатчик плохо питать от ножки МК. Никакой формально подтвержденной теории на этот счет у меня нет, без осциллографа, которого у меня нет, выяснить точно, наверное, нет возможности, но рабочая гипотеза такая. 30mA, заявленные для передатчика доблестными китайцами могут означать все, что угодно. Например, среднее потребление при передаче какой-нибудь тестовой последовательности. При этом пиковое потребление, при передаче единицы может быть каким угодно, в том числе больше 40mA. С другой стороны, я уверен, что 40mA, заявленные для вывода МК – это точно верхняя граница, за пределами которой ничего не гарантируется. Так что вполне возможно, что пиковое потребление передатчика могло запросто быть больше возможностей МК. Стоит отметить, что из двух недель конструирования датчика (изначально планировалась пара дней) я 90% времени потратил на решение проблем с передатчиком.
Тривиальная электрическая схема выглядит так:
image

Исходный код датчика

#include <avr/sleep.h>
#include <avr/wdt.h>
#include <DHT.h>
#include <VirtualWire.h> //workaround for the stupid arduino problem of not being able to include a library from another library, so i'm including the libs needed by WSP here
#include <EEPROM.h>
#include <WirelessSensorPipe.h>

//#define DEBUG
#define KIDROOM
//#define OUTDOOR
  
/* hardware configuration */
#define ACCESSORIESPOWERPIN 3
#define DHTPIN 4
#define TRANSMITPIN 2

#if defined (KIDROOM)
#define DHTTYPE DHT22
#define TCORRECTION (0)
#define HCORRECTION (0)

#elif defined (OUTDOOR)
#define DHTTYPE DHT22
#define TCORRECTION (0)
#define HCORRECTION (0)
#endif

#ifdef DEBUG
#define SLEEPDURATION 0 //sleep duration in seconds, shall be a factor of 8
#else
#define SLEEPDURATION 320 //sleep duration in seconds, shall be a factor of 8
#endif

DHT dht(DHTPIN, DHTTYPE);
void sleepFor8Secs()
{
  // disable ADC
  ADCSRA = 0;  

  // clear various "reset" flags
  MCUSR = 0;     
  // allow changes, disable reset
  WDTCSR = bit (WDCE) | bit (WDE);
  // set interrupt mode and an interval 
  WDTCSR = bit (WDIE) | bit (WDP3) | bit (WDP0);    // set WDIE, and 8 seconds delay
  wdt_reset();  // pat the dog

  set_sleep_mode (SLEEP_MODE_PWR_DOWN);  
  sleep_enable();

  // turn off brown-out enable in software
  MCUCR = bit (BODS) | bit (BODSE);
  MCUCR = bit (BODS); 
  sleep_cpu ();  

  // cancel sleep as a precaution
  sleep_disable();
}

// watchdog interrupt
ISR (WDT_vect) 
{
  wdt_disable();  // disable watchdog
}  // end of WDT_vect

WirelessSensorPipe pipe;
void setup () 
{
#ifdef DEBUG
  Serial.begin(9600);
  Serial.println("Entered setup");  
#endif
  dht.begin();
  pinMode(ACCESSORIESPOWERPIN, OUTPUT);
  pipe.begin(TRANSMITPIN, 0);

#ifdef DEBUG
  Serial.print("Sensor id:");  
  Serial.print(pipe.id());  
#endif

}

void loop()
{
  pinMode(ACCESSORIESPOWERPIN, OUTPUT);
  digitalWrite(ACCESSORIESPOWERPIN, HIGH); //turn on the DHT sensor and the transmitter
  delay(2000); //sleep till the intermediate processes in the accessories are settled down

  float humidity = dht.readHumidity() + HCORRECTION;
  float temperature = dht.readTemperature() + TCORRECTION;

  digitalWrite(ACCESSORIESPOWERPIN, LOW); // turn off the accessories power
  pinMode(ACCESSORIESPOWERPIN, INPUT); //change pin mode to reduce power consumption

#ifdef DEBUG
  Serial.print("Humidity: ");
  Serial.println(humidity);
  Serial.print("Temperature: ");
  Serial.println(temperature);

  /* blink the LED to indicate that the readings are done */
  digitalWrite(13, HIGH);
  delay(100);
  digitalWrite(13, LOW);
#endif
  
  pipe.send(WirelessSensorPipe::TEMPERATURE, temperature);
  delay(1000);

  pipe.send(WirelessSensorPipe::HUMIDITY, humidity);

  for (int i = 0; i < SLEEPDURATION / 8; i++)
    sleepFor8Secs();
}

Внимательный читатель заметит, что в прошивке сделана возможность ввести поправки для данных, считанных с DHT. Дело в том, что DHT11, как выяснилось, обладают рядом недостатков, а именно: точность до целых градуса, крайне плохая калибровка и повторяемость результатов от измерения к измерения и от датчика к датчику. Так что два DHT11, стоящих рядом могут показывать температуру с разницей в два градуса, что меня не устраивало. Кроме того, забегая вперед, в тему следующей статьи, скажу, что у меня дом отапливается котлом, который в тот момент управлялся руками, т.е. мы исходя из обстановки на улице выставляли желаемую мощность котла. Почти все современные котлы имеют возможность управления, по крайней мере, в режиме on/off. Так что следующий прожект – использовать имеющиеся данные о температуре на улице и в комнатах для термостатирования обстановки внутри помещения. С этой целью мерить температуру внутри помещения с точностью до градуса – очень грубо, такая грубость приводит к автоколебаниям в системе термостатирования. Так что было решено заменить DHT11 на гораздо более дорогие DHT22, которые не требуют никаких поправок (два DHT22, стоящие в одном месте совпадают в показаниях вплоть до одной десятой градуса) и в десять раз точнее.

Фотографии сенсора

image
image
image

Центральный юнит

Итак, погодные данные в эфире имеются. Вопрос в том, что с ними делать дальше. А дальше их надо поймать, инкапсулировать в HTTP и передать на хранение в имеющийся дома сервер. Для этого было решено сделать центральный юнит, оснащенный Arduino Nano, суперрегенеративным приемником на 433МГц и Ethernet-модулем ENC28J60. Использование Nano вместо Pro Mini было продиктовано наличием преобразователя на 3V3 на плате Nano, а ENC28J60 не смотря на толерантность к +5V на информационных пинах все же требует питания в 3V3.
Схема выглядит так:
image
Как видно, кроме ENC28J60 схема содержит также датчик DHT (в конечном итоге это DHT22) для снятия температуры/влажности в месте дислокации центрального юнита и датчик BMP085, используемый для измерения давления. Из особенностей аппаратной реализации стоит отметить, что BMP085 бывают разные, сам датчик питается от 3V3, но я купил со встроенным преобразователем 5V->3V3, так что его можно было питать обоими напряжениями. В случае Nano это неактуально, потому что у него есть собственный преобразователь, но в случае использования Pro Mini это была бы полезная фича.
С хардверной частью сюрпризов не было, а вот с программной – хоть отбавляй. Сюрпризы связаны с Ethernet-контроллером. Во-первых, от широко используемого в Ethernet-shield’ах W5100 ENC28J60 отличается тем, что не содержит хардверной реализации TCP/IP, стек реализован программно, библиотекой для работы с этим чипом. Во-вторых, весь community-опыт сосредоточен вокруг реализации примитивных HTTP-серверов на Arduino, у меня же задача обратная, сделать Arduino-«браузер», который через HTTP GET запрос отдавал бы данные на сервер. Попытка реализовать такой браузер натолкнулась на приличное количество трудностей. Не смотря на то, что библиотека ethercard определяет метод browseUrl, который делает то, что нужно, работа его не отличается стабильностью. Сам по себе метод лишь означает запрос к библиотеке на формирование GET-запроса. Для того, чтобы выполнить сам запрос, нужно определить callback-метод, вызываемый по получении данных от сервера и после обращения к browseURL вызывать метод packetLoop, который осуществляет непосредственную прокачку траффика. Так вот, теоретически работа по посылке сообщения состоит из двух этапов: однократного вызова browseUrl и многократного вызова packetLoop для прокачки запроса и получения ответа. Совершенно непонятно, сколько раз нужно дергать packetLoop. Никаких статусов метод не возвращает. На вход принимает длину пакета, которую можно определить вызовом метода packetReceive. Первая мысль была, что нужно дергать packetReceive и packetLoop до тех пор, пока packetReceive возвращает не ноль. Но нет, во-первых, сразу после вызова browseUrl множество вызовов packetReceive возвращает ноль. Во-вторых, после появления первого не ноля, может идти серия нолей, затем опять не ноль. Поскольку мне ответ от сервера не интересен, главная проблема – вызвать packetLoop количество раз, необходимое для тансфера GET-запроса к серверу, трансфер же в обратном направлении может быть пропущен. Тут помог callback, который ethercard вызывает при появлении данных в буфере ENC28J60. Достаточно дождаться первого вызова, чтобы быть уверенным, что трансфер в направлении сервера состоялся, и началась передача в обратном направлении. После этого вызовы packetLoop можно заканчивать. Проблема в том, что иногда callback не вызывается после browseUrl и тогда в петле вызова packetLoop можно находится вечно. Пришлось ограничить эту процедуру по времени. Если Arduino не получает ответа от сервера в течение 5 секунд, он бросает текущую попытку запроса. Еще один весьма неприятный сюрприз заключается в том, что время от времени (воспроизводится крайне нестабильно, но раз в сутки случается точно) тандем ENC28J60/ethercard впадает в устойчивое состояние, когда ничего не передается и не принимается. Лечится это перезагрузкой МК. Так что пришлось считать неудачные попытки передачи и в случае накопления большого их количества принудительно перегружать контроллер.
Со всеми этими ухищрениями центральный юнит работает стабильно, никакого вмешательства в его работу я уже месяц не делаю.

Скетч центрального юнита
#include <Wire.h>
#include <DHT.h>
#include <VirtualWire.h>
#include <EtherCard.h>
#include <EEPROM.h>
#include <Time.h>
#include <WirelessSensorPipe.h>
#include <Adafruit_BMP085.h>


#define DEBUG

/* Hardware configuration */
#define RECEIVEPIN 2
#define DHTPIN 4
#define DHTTYPE DHT22

/* Sensor corrections */
#define TCORRECTION (0)
#define HCORRECTION (0)

/* Timeouts */
#define RECEIVETIMEOUT 30 // wireless receive timeout
#define MEASUREUPDATEPERIOD 300 //self measuring period

/* Own sensors */
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP085 bmp;

/*Sensor pipe*/
WirelessSensorPipe pipe;

/* Ethercard stuff */
#define BUFFER_SIZE 400
byte Ethernet::buffer[BUFFER_SIZE];
#define FAILEDSENDATTEMPTSALLOWED 10 //if it fails to send data more than FAILEDSENDATTEMPTSALLOWED attempts the reboot is forced

static uint8_t mymac[6] = { 
  0x54,0x55,0x58,0x12,0x34,0x56 };
char PROGMEM websrvip_str[] = "192.168.1.250";

byte answer_received = 0;
// called when the client request is complete
static void my_callback (byte status, word off, word len) {
#ifdef DEBUG
  Serial.print(F("HTTP GET status: "));
  Serial.println(status);
  //  Ethernet::buffer[off+300] = 0;
  //  Serial.print((const char*) Ethernet::buffer + off);
#endif
  answer_received = 1;
}

void(* resetFunc) (void) = 0;
int num_of_failed_send_requests = 0;
void sendSensorData(int sensor_id, int sensor_type, float data)
{
  char buffer[40];
  char conv_buffer[11];
  buffer[0] = 0;
  strcat_P(buffer, PSTR("?script=updS"));
  strcat_P(buffer, PSTR("&id="));
  strcat(buffer, itoa(sensor_id, conv_buffer, 10));
  strcat_P(buffer, PSTR("&t="));
  strcat(buffer, itoa(sensor_type, conv_buffer, 10));
  strcat_P(buffer, PSTR("&v="));
  strcat(buffer, dtostrf(data, 2, 2, conv_buffer));
#ifdef DEBUG
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.print(second());
  Serial.print("    :");
  Serial.print(F("Sending request with params: "));
  Serial.println(buffer);
#endif
  answer_received = 0;
  ether.browseUrl(PSTR("/objects/"), buffer, websrvip_str, &my_callback);
  int packet_len = 1;
  int begin_waiting_time = now();
  while(!answer_received || packet_len != 0)
  {
    packet_len = ether.packetReceive();
    ether.packetLoop(packet_len);
    
    if (now() - begin_waiting_time > 5)
    {
      num_of_failed_send_requests++;
      Serial.print(F("Failed to send data "));
      Serial.print(num_of_failed_send_requests);
      Serial.println(F(" times"));
      if (num_of_failed_send_requests >= FAILEDSENDATTEMPTSALLOWED)
      {
        Serial.println(F("Resetting the device"));
        resetFunc();
      }
      break;
    }
  }
}

void setup () 
{
#ifdef DEBUG
  Serial.begin(9600);
  Serial.println(F("Entered setup"));
#endif

  pipe.begin(0, RECEIVEPIN);

#ifdef DEBUG
  Serial.print("Sensor id:");  
  Serial.println(pipe.id());  
#endif

  dht.begin();
  bmp.begin();

  if (ether.begin(sizeof Ethernet::buffer, mymac) == 0)
  {
    Serial.println(F("Failed to access Ethernet controller"));
    resetFunc();
  }
  
  if (!ether.dhcpSetup())
  {
    Serial.println(F("DHCP failed"));
    resetFunc();
  }
  ether.printIp(F("IP:  "), ether.myip);
  ether.printIp(F("GW:  "), ether.gwip);  
  ether.printIp(F("DNS: "), ether.dnsip);  
  if (!ether.dnsLookup(websrvip_str))
  {
    Serial.println(F("DNS failed"));
    resetFunc();
  }
  ether.printIp(F("SRV: "), ether.hisip);
}


time_t previous_measure_time = -1;
void loop()
{
  time_t current_time = now();
  if(current_time - previous_measure_time > MEASUREUPDATEPERIOD)
  {
    float dhttemperature = dht.readTemperature() + TCORRECTION;
    float humidity = dht.readHumidity() + HCORRECTION;
    float temperature = bmp.readTemperature();
    float pressure = bmp.readPressure()/133.33;

    sendSensorData(pipe.id(), 500, temperature);
    sendSensorData(pipe.id(), WirelessSensorPipe::TEMPERATURE, dhttemperature);
    sendSensorData(pipe.id(), WirelessSensorPipe::HUMIDITY, humidity);
    sendSensorData(pipe.id(), WirelessSensorPipe::PRESSURE, pressure);
    previous_measure_time = current_time;
  }
  
  WirelessSensorPipe::Packet packet;
  if (pipe.receive(packet, RECEIVETIMEOUT * 1000))
  {
    sendSensorData(packet.id, packet.type, packet.value);
  }
}

Фотографии готового девайса

image
image

Как видно, сбоку болтается DHT22, который нельзя поместить на корпус и уж тем более вовнутрь корпуса, поскольку устройство генерирует небольшое, но достаточное количество тепла, чтобы заставить температурный датчик врать. В качестве примера – показания температуры с датчика BMP085, расположенного внутри корпуса, которые стабильно на 2 градуса выше правильного значения.

Серверная часть

Первая мысль, которая приходит в голову, когда собираешься сделать штуку для хранения чиселок с последующим их отображением: ты ведь наверняка не первый, кто пытается изобрести это колесо. Бриф-инвестигейт интернета показал, что спектр решений довольно широк, от «а ты че, не программер чтоли? вот тебе VisualBasic, запрограммируй», до «вот здесь вот чуть-чуть поднастройте и все работает из коробки». Истина, как всегда, где-то посередине. С одной стороны, понятно, что чтобы сделать все красиво, вложиться в код придется основательно, это запросто может погасить всякий энтузиазм. С другой – коробочные решения никогда не делают чего хочется, не хватает кастомизируемости. Я медленно подвел к тому, что есть замечательный проект Majordomo (smartliving.ru), цель которого – создание платформы для умного дома. «Чем достаточно умная метеостанция – не часть умного дома?» подумал я и сел за изучение вопроса. Прелесть Majordomo IMHO в том, что достаточно простые, типовые вещи там работают с небольшой настройкой и прямо из коробки. Но всегда остается возможность, если чего-то не нравится, дописать (благо есть достаточно развитые средства скриптования на PHP) или переписать (платформа открытая, исходный код доступен). Из средств, поддерживаемых Majordomo пригодились объекты, данные в которых можно обновлять извне через HTTP-запрос и домашние страницы, на которых можно нарисовать все, что душе угодно и поддерживается HTML/JS/CSS и остальным зоопарком web-технологий.
Под сенсоры на Arduino был организован отдельный класс объектов, обладающих вот такой структурой:
image
DeviceID – это тот самый адрес устройства, который участвует в микропротокольном пакете. Описание остальных полей говорит само за себя. Логика работы такая:
Когда центральный юнит передает данные, среди объектов этого класса отыскивается такой, у которого DeviceID и SensorType совпадают с переданными. Для найденного объекта выставляется переданное значение (значение из поля float пакета микропротокола), UpdatedTime выставляется в Now, Actual выставляется в 1, взводится таймер на время ActualityPeriod в будущем, который сбросит Actual в 0. Последний фокус необходим для того, чтобы понять, что значение сенсора неактуально, поскольку давно не обновлялось, в случае, если с центральным юнитом или самим сенсором что-нибудь случилось. Таких датчиков имеется несколько:
image
Значения этих сенсоров затем визуализируются при помощи домашних страниц. Я использовал различные страницы для отображения различных данных, либо одних и тех же данных, но в виде, оптимизированном для отображения на различных устройствах.

Устройства отображения

В качестве центрального устройства отображения был использован покрывшийся изрядным количеством пыли старенький Dell Streak 5, который [референс в начало статьи, в абзац с выбором устройства для отображения] выглядит значительно более выгодно, чем экранчик в 1.8 или 2.2 дюйма, прикрученный непосредственно к Arduino и в своем максимуме способный выводить текст и иконки 8x8. Будучи настроенным на автозапуск браузера Dolphin на старте, который открывает страницу с погодой, он стал готовым лицом погодной станции (и не только погодной станции, опять анонсирую будущую статью про термостатирование). Тут надо отметить, что в тексте соответствующей домашней страницы Majordomo пришлось прибегнуть к набору трюков, которые переводят браузер в полноэкранный режим и скрывают все лишнее, вроде адрес-бара и строки состояния Android.
Кроме того, данные просматриваются на iPhone и iPad в основном супругой, с целью мониторинга климатической обстановки в детской комнате.

Итог этой технологической солянки в фотографиях

Страница с погодой на Dell Streak:
image

Фото Streak:
image

Тапнув по любому из индикаторов можно посмотреть динамику изменения показателя за последние 12 часов, пример для температуры на улице:
image

Погода на iPhone:
image

Тестирование, выводы, планы

К текущему моменту система эксплуатируется около полутора месяцев. Особых нареканий к ней нет, кроме случившегося два раза зависания центрального юнита с полным отключением ENC28J60 (не горели даже светодиоды линка и активности). Лечилось это снятием питания с юнита на несколько секунд. В причины такого поведения записал не очень хорошее качество электричества в бортовой сети дома, которое иногда «дребезжит», т.е. пропадает на какие-то доли секунды. Как поведет себя электроника в таких условиях – не известно. В качестве средств предотвращения влияние этого фактора была задумана установка двух ионисторов на 1Ф (одного для питания контроллера 5V, второй – для Ethernet-адаптера, 3V3). Можно было обойтись резервом в виде аккумуляторов, но это намного дороже (стоимость ионисторов – десятки рублей, комплект нормальных аккумуляторов обойдется в полтысячи), не интересно (аккумуляторы я видел много раз, а ионисторы вижу впервые) и не соответствует цели (поскольку цель – предотвратить влияние кратковременных, в несколько секунд, мерзких эффектов в сети, а не питать юнит в отсутствие электричества).
В выводы можно записать, что комплект железок со своей задачей справляется и имеет резерв для расширения в следующих направлениях:

  • Расширение сети датчиков. Центральный юнит способен принимать любые данные, пересылаемые посредством микропротокола, а значит можно задумывать не только погодные датчики. Пример: счетчик воды и модуль сопряжения к счетчику электричества, имеющему понятный физический интерфейс – RS232 и непонятный пока протокол обмена
  • Введение в систему исполнительных устройств

В ближайших планах намечены:

  • Контроллер котла, который вместе с погодными датчиками образует замкнутую систему термостатирования в доме. Прототип уже сделан и проходит испытания. Этой поделке обещаю посвятить отдельную статью
  • Упомянутые счетчики электричества и воды с модулем обнаружения протечек

Автор: Sermus

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js