habrahabr

Погодная станция на Ethernet (HTTP+Modbus) с питанием по POE

  • пятница, 28 марта 2014 г. в 03:10:31
http://habrahabr.ru/post/214011/

Доброго времени суток хабр-сообщество.
С момента моего последнего поста про умный дом прошло много времени. Я решил его делать начиная с погодной станции.


Рисунок 1 — Фотография макетного образца

Несмотря на обилие статей про погодные станции на arduino (http://habrahabr.ru/post/165747/, habrahabr.ru/post/171525/, habrahabr.ru/post/213405/ ) Я все-таки решил опубликовать своё решение.

Функционал


Функции которые она выполняет:
  • Измерение температуры
  • Измерение влажности
  • Измерение давления
  • Измерение освещенности
  • Индикация измеренных параметров
  • Выдача измеренных параметров по интерфейсу HTTP в виде XML документа
  • Выдача по протоколу HTTP XSLT процессора для стилизации XML при отображении браузером
  • Выдача информации по Modbus (его предполагаю использовать в качестве протокола управления умным домом)
  • Питание через Passive POE


Функции которые не удалось реализовать, но хотелось:
  • Измерение наличие осадков
  • Направление ветра
  • Скорость ветра
  • DHCP — клиент

Стоимость ~1700руб (по курсу на момент покупки), в него входило:
  1. DHT22 Digital Temperature And Humidity Sensor $ 5.13
  2. GY-65 BMP085 Atmospheric Pressure Altimeter Module $ 4.09
  3. GY-302 BH1750 Chip Light Intensity Light Module $ 2.85
  4. IBOARD W5100 Ethernet Module for Arduino Development Board with POE / Xbee and SD Card Slot Expansion $ 18.46
  5. MAX7219 Digital Tube Display Module$ 4.74
  6. 40pcs 20cm 2.54mm 1p-1p pin Dupont wire cable Line connector $ 2.42
  7. FT232RL USB To Serial Line Download Line Downloader USB TO 232 $ 4.47
  8. Корпус пластиковый ~100 руб


Реализация устройства

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

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

В качестве мозгов выбрана плата Iboard w5100, по причине того, что в ней уже присутствует весь функцонал arduino + Ethernet shield + sensor board для размножения питания и земли, при подключении большого количества устройств. Такая высокоинтегрированная плата съэкономит деньги и место. Также эта плата поддерживает работу с Passiv POE.

Передачу данных я решил сделать проводным (в отличие от большинства решений умного дома) по следующим причинам:
  1. Провод в любом случае тянуть — будь то питания или передачи данных
  2. В случае если решено запитать устройство от химических источников питания — то будет возникать вопрос их замены
  3. Проводные коммуникации кажутся более надёжными:
    1. К ним не будет претензий со стороны Роскомнадзора, так как ничего не излучают в эфир
    2. Не возникнет проблем со взаимным влиянием других приемо-передатчиков в той-же частоты
    3. Не возникнет огромного количества проблем связанных с реализацией радиосвязи (временного разделения эфира, затухания сигнала, влиение других таких-же систем при перекрывании зон приёма, и.т.д.)
    4. Будут спокойны люди, которым Wifi греет мозг.

Чтобы не плодить интерфейсы, и все сделать единообразно — я решил для передачи данных использовать Ethernet. Питание передавать по этому-же проводу, используя технологию Passiv POE. Достоинства этого способа — если все устройства будут подключены в общую Ethernet сеть — то не возникнет вопроса с протокольными конвекторами/шлюзами.


Рисунок 2 — Блок схема устройства


Рисунок 3 — схема соединения


Рисунок 4 — Вид изнутри

Реализация Passiv POE

Passive POE — Это особая реализация модули POE. В оригинальном POE требуется реализация протокола, при этом инжектор при установки связи определяет мощность удаленного оборудования, и если он может его запитать — то записывает. Passive POE придумали хитрые китайцы, которые не хотели делать такую хитрую реализацию, но хотели выйти на рынок POE оборудования, а результате они придумали тупо подать напряжение на неиспользуемые витые пары кабеля категории 5E. Некоторые даже пишут что они якобы придерживаются стандарта IEEE 802.3af в части электрических характеристик, однако даже это не всегда так.

IBOARD W5100 работает, если на выводы 4,5 и 7,8 разъема Ethernet подать напряжение 6-20В. Я подаю 20 В.


Рисунок 5 — Схема соединения для инжектирования POE

Инжектор я вмонтировал в D-LINK 320, установив в него китайский повышающий DC-DC преобразователь.


Рисунок 6 — Фотография модификации

После монтажа — все заработало.

Внимание! Перед включением проверьте, что источник питания настроен на выдачу 20В.


Рисунок 7 — Работы всей конструкции

Реализация WEB-интерфейса

Web-интерфейс был сделан, для наиболее удобного взаимодействия с устройством из сети без дополнительного ПО на стороне клиента. Для возможности автоматизированного доступа к данным было бы удобно выдавать информацию в виде XML файла. В данном проекта было совмещено эти два способа доступа к данным посредством использования XSLT процессора.

Web-браузер посылает запрос на 192.168.0.20/; Arduino в ответ отправляет XML документ:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="http://192.168.0.20/z1.xsl"?>
<response>
	<temperature>
		<celsius>30.70</celsius>
		<sensors>
			<sensor name='BMP' unit='C'>32.62</sensor>
			<sensor name='DHT' unit='C'>30.70</sensor>
		</sensors>
	</temperature>
	<humidity>
		<percentage>21.60</percentage>
	</humidity>
	<pressure>
		<pa>99309</pa>
		<mmHg>745</mmHg>
	</pressure>	<illuminance>
		<lx>11</lx>
	</illuminance>
</response>


Для отображения этой информации в красивом виде браузер загружает XSLT процессор, и с помощью него генерирует HTML документ.
Исходник

<?xml version='1.0' encoding='UTF-8'?>
<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
<xsl:template match='/'>
  <html>
  <head>
	  <title>Weather station</title>
     <meta http-equiv='refresh' content='5'/>
	  <style>
	   .z1 {
		font-family:Arial, Helvetica, sans-serif;
		color:#666;			font-size:12px;
		text-shadow: 1px 1px 0px #fff;
		background:#eaebec;
		margin:20px;	
		border:#ccc 1px solid;	
		border-collapse:separate;
 		border-radius:3px;
		box-shadow: 0 1px 2px #d1d1d1;
	   }
	 .z1 th {	
		font-weight:bold;
		padding:15px;
		border-bottom:1px solid #e0e0e0;
		background: #ededed;
		background: linear-gradient(to top,  #ededed,  #ebebeb);
	   }
	 .z1 td {
		padding:10px;
		background: #f2f2f2;
		background: linear-gradient(to top,  #f2f2f2,  #f0f0f0); 
 	   }
	.z1 tr:hover td{
		background: #aaaaaa;
		background: linear-gradient(to top, #f2f2f2,  #e0e0e0);
  	}
	  </style>
  </head>
  <body>
  <h2>Weather station</h2>
    <table class='z1'>
      <tr>
        <th>Property</th>
        <th>Value</th>
      </tr>
      <tr>
        <td> Temperature </td>
        <td><xsl:value-of select='response/temperature/celsius'/> C</td>
      </tr>
      <tr>
        <td> Humidity </td>
        <td><xsl:value-of select='response/humidity/percentage'/> %</td>
      </tr>
      <tr>
        <td> Pressure </td>
        <td><xsl:value-of select='response/pressure/mmHg'/> mm.Hg</td>
      </tr>
      <tr>
        <td> Illuminance </td>
        <td><xsl:value-of select='response/illuminance/lx'/> lx</td>
      </tr>
    </table>
	<h2>Termosensor</h2>
    <table class='z1'>
      <tr>
        <th>Sensor</th>
        <th>Value</th>
      </tr>
	  <xsl:for-each select='response/temperature/sensors/sensor'>
      <tr>
        <td> <xsl:value-of select='@name'/> </td>
        <td><xsl:value-of select='.'/> <xsl:value-of select='@unit'/></td>
      </tr>
	  </xsl:for-each>
    </table>
  </body>
  </html>
</xsl:template>
</xsl:stylesheet>



В результате всех этих мероприятий этот XML документ в Web-браузере выглядит вот так:

Рисунок 8 — Внешний вид web-интерфейса в Firefox

Программное обечпечение


В ПО я старался по максимуму использовать готовые библиотеки, чтобы ускорить процесс.

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

В цикле считываем все показания датчиков, если необходимо — обновляем данные на 8-разрядном LED-индикаторе и обрабатываем запрос по сети Ethernet.

В проекте были использованы следующие библиотеки:
  • DHTLib — Библиотека доступа к датчику температуры/влажности
  • Adafruit-BMP085-Library — Библиотека доступа к датчику давления/температуры
  • BH1750 — Библиотека доступа к датчику освещенности
  • LedControl — Библиотека работы с LED индикатором по птоколу SPI
  • Webduino — Библиотека реализации протокола HTTP
  • Mudbus — Библиотека реализации протокола Modbus


Стоит заметить, что для вывода букв — мне пришлось модернизировать таблицу символов в библиотеке LedControl. По умолчанию эта библиотека может отображать только буквы a-f.

Исходник
#include <Ethernet.h>
#include <SPI.h>
#include <Wire.h>
#include "DHT.h"
#include "BH1750.h"
#include "Adafruit_BMP085.h"
#include "LedControl.h"
#include "Mudbus.h"
#include "WebServer.h"

#define WEATHER_STATION_Z1 0x20

// ===============================================================
#define DHT_S1_PIN A0    // пин для датчика DHT22
// ===============================================================
// assign a MAC address for the ethernet controller.
// fill in your address here:
byte mac[] = { 
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
// assign an IP address for the controller:
IPAddress ip(192,168,0,20);
IPAddress gateway(192,168,0,1);	
IPAddress subnet(255, 255, 255, 0);
// ===============================================================
float humidity = 0, temp_dht = 0, temp_bmp = 0, temp = 0;
uint16_t light = 0;
int32_t pressure_pa = 0, pressure_mm = 0;
int mode = 0;

dht dht_s1;

BH1750 lightMeter;

Adafruit_BMP085 bmp;

/* This creates an instance of the webserver.  By specifying a prefix
 * of "", all pages will be at the root of the server. */
#define PREFIX ""
WebServer webserver(PREFIX, 80);
//EthernetServer webserver(80);

#define DEV_ID Mb.R[0]
#define TEMPERATURE Mb.R[1]
#define TEMPERATURE_DHT Mb.R[2]
#define TEMPERATURE_BMP Mb.R[3]
#define HUMIDITY Mb.R[4]
#define PRESSURE_MM Mb.R[5]
#define LIGHT Mb.R[6]
Mudbus Mb;

// pin A5 is connected to the DataIn 
// pin A6 is connected to the CLK 
// pin A7 is connected to LOAD 
LedControl lc=LedControl(A1,A2,A3,1);

// ======================== Web pages ==========================
void web_index(WebServer &server, WebServer::ConnectionType type, char *, bool)
{
  /* this line sends the standard "we're all OK" headers back to the
     browser */
  server.httpSuccess("application/xml; charset=utf-8");

  /* if we're handling a GET or POST, we can output our data here.
     For a HEAD request, we just stop after outputting headers. */
  if (type != WebServer::HEAD)
  {
    /* this defines some HTML text in read-only memory aka PROGMEM.
     * This is needed to avoid having the string copied to our limited
     * amount of RAM. */
    P(index_p1) = 
      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
      "<?xml-stylesheet type=\"text/xsl\" href=\"http://192.168.0.20/z1.xsl\"?>"
      "<response>"
      "	<temperature>"
      "		<celsius>";
    P(index_p2) = "</celsius>"
      "		<sensors>"
      "			<sensor name='BMP' unit='C'>";
    P(index_p3) = "</sensor>"
      "			<sensor name='DHT' unit='C'>";
    P(index_p4) = "</sensor>"
      "		</sensors>"
      "	</temperature>"
      "	<humidity>"
      "		<percentage>";
    P(index_p5) = "</percentage>"
      "	</humidity>"
      "	<pressure>"
      "		<pa>";
    P(index_p6) = "</pa>"
      "		<mmHg>";
    P(index_p7) = "</mmHg>"
      "	</pressure>"
      "	<illuminance>"
      "		<lx>";
    P(index_p8) = "</lx>"
      "	</illuminance>"
      "</response>";
    /* this is a special form of print that outputs from PROGMEM */
    server.printP(index_p1);
    server.print(temp);
    server.printP(index_p2);
    server.print(temp_bmp);
    server.printP(index_p3);
    server.print(temp_dht);
    server.printP(index_p4);
    server.print(humidity);
    server.printP(index_p5);
    server.print(pressure_pa);
    server.printP(index_p6);
    server.print(pressure_mm);
    server.printP(index_p7);
    server.print(light);
    server.printP(index_p8);
  }
}
void web_z1_xsl(WebServer &server, WebServer::ConnectionType type, char *, bool)
{
  server.httpSuccess("text/xsl; charset=utf-8");
  if (type != WebServer::HEAD)
  {
    P(z1_xsl) = 
    "<?xml version='1.0' encoding='UTF-8'?>"
    "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>"
    "<xsl:template match='/'>"
    "  <html>"
    "  <head>"
    "	  <title>Weather station</title>"
    "     <meta http-equiv='refresh' content='5'/>"
    "	  <style>"
    "	   .z1 {"
    "			font-family:Arial, Helvetica, sans-serif;"
    "			color:#666;"
    "			font-size:12px;"
    "			text-shadow: 1px 1px 0px #fff;"
    "			background:#eaebec;"
    "			margin:20px;"
    "			border:#ccc 1px solid;"
    "			border-collapse:separate; "
    "			border-radius:3px;"
    "			box-shadow: 0 1px 2px #d1d1d1;"
    "	   }"
    "	   .z1 th {"
    "			font-weight:bold;"
    "			padding:15px;"
    "			border-bottom:1px solid #e0e0e0;"
    "			background: #ededed;"
    "			background: linear-gradient(to top,  #ededed,  #ebebeb);"
    "	   }"
    "	   .z1 td {"
    "			padding:10px;"
    "			background: #f2f2f2;"
    "			background: linear-gradient(to top,  #f2f2f2,  #f0f0f0);  "
    "	   }"
    "		.z1 tr:hover td{"
    "			background: #aaaaaa;"
    "			background: linear-gradient(to top, #f2f2f2,  #e0e0e0);  "
    "		}"
    "	  </style>"
    "  </head>"
    "  <body>"
    "  <h2>Weather station</h2>"
    "    <table class='z1'>"
    "      <tr>"
    "        <th>Property</th>"
    "        <th>Value</th>"
    "      </tr>"
    "      <tr>"
    "        <td> Temperature </td>"
    "        <td><xsl:value-of select='response/temperature/celsius'/> C</td>"
    "      </tr>"
    "      <tr>"
    "        <td> Humidity </td>"
    "        <td><xsl:value-of select='response/humidity/percentage'/> %</td>"
    "      </tr>"
    "      <tr>"
    "        <td> Pressure </td>"
    "        <td><xsl:value-of select='response/pressure/mmHg'/> mm.Hg</td>"
    "      </tr>"
    "      <tr>"
    "        <td> Illuminance </td>"
    "        <td><xsl:value-of select='response/illuminance/lx'/> lx</td>"
    "      </tr>"
    "    </table>"
    "	<h2>Termosensor</h2>"
    "    <table class='z1'>"
    "      <tr>"
    "        <th>Sensor</th>"
    "        <th>Value</th>"
    "      </tr>"
    "	  <xsl:for-each select='response/temperature/sensors/sensor'>"
    "      <tr>"
    "        <td> <xsl:value-of select='@name'/> </td>"
    "        <td><xsl:value-of select='.'/> <xsl:value-of select='@unit'/></td>"
    "      </tr>"
    "	  </xsl:for-each>"
    "    </table>"
    "  </body>"
    "  </html>"
    "</xsl:template>"
    "</xsl:stylesheet>";

    /* this is a special form of print that outputs from PROGMEM */
    server.printP(z1_xsl);
  }
}

// ========================СТАРТУЕМ=============================
void setup(){
  // Init LED display
  lc.shutdown(0,false);
  lc.setIntensity(0,2);
  lc.clearDisplay(0);
  lc.setChar(0,7,'L',false);
  lc.setChar(0,6,'O',false);
  lc.setChar(0,5,'A',false);
  lc.setChar(0,4,'d',false);
  //запускаем Ethernet
  SPI.begin();
  
  Ethernet.begin(mac, ip);
  // Init Light sensor
  lightMeter.begin();
  // Init pressure sensor
  if (!bmp.begin()) {
    Serial.println("ERROR: BMP085 sensor failed");
  }

  //enable serial datada print
  Serial.begin(9600); 
  Serial.println("Weather Z1 v 0.1"); // Тестовые строки для отображения в мониторе порта
  
  webserver.setDefaultCommand(&web_index);
  webserver.addCommand("index.html", &web_index);
  webserver.addCommand("z1.xsl", &web_z1_xsl);
  
  webserver.begin();
}

void loop(){
  char buff[64];
  int len = 64;
  
  mode = (mode + 1) % 100;

  Z1_sensors_update();
  
  Z1_SerialOutput();
  
  Z1_ledDisplay();
  
  Z1_modbus_tcp_slave();
  
//  Z1_http_server();

  webserver.processConnection(buff, &len);
}

void Z1_sensors_update() {
 if (mode%30==0) {

  // BH1750 (light)
  light = lightMeter.readLightLevel();
  // BMP085 (Temp and Pressure)
  temp_bmp = bmp.readTemperature();
  pressure_pa = bmp.readPressure();
  pressure_mm = pressure_pa/133.3;
  // DHT22 (Temp)
  if (dht_s1.read22(DHT_S1_PIN) == DHTLIB_OK) {
    humidity = dht_s1.humidity;
    temp_dht = dht_s1.temperature;
    temp = temp_dht;
  } else {
    temp = temp_bmp;
  }
  
 }
}

void Z1_SerialOutput() {
  Serial.print("T1= "); 
  Serial.print(temp_dht);
  Serial.print(" *C \t");
  Serial.print("T2= "); 
  Serial.print(temp_bmp);
  Serial.print(" *C \t");
  Serial.print("Pressure= "); 
  Serial.print(pressure_mm);
  Serial.print(" mm \t");
  Serial.print("Humidity= "); 
  Serial.print(humidity);
  Serial.print(" %\t");
  Serial.print("Light= "); 
  Serial.print(light);
  Serial.print(" lx \t");
  Serial.print("\n");

}

void Z1_ledDisplay() {
  int v;
  
  if (light<50) {
    lc.setIntensity(0,0);
  } else if (light>80 && light<200) {
    lc.setIntensity(0,2);
  } else if (light>250 && light<1000) {
    lc.setIntensity(0,5);
  } else if (light>1100) {
    lc.setIntensity(0,15);
  }
  
  if (mode<=25) {
  //  lc.clearDisplay(0);
    lc.setChar(0,7,'t',false);
    if (temp>=0) {
      lc.setChar(0,6,' ',false);
    } else {
      lc.setChar(0,6,'-',false);
    }
    v = (int)( temp / 10 ) % 10;
    lc.setDigit(0,5,(byte)v,false);
    v = (int)( temp  ) % 10;
    lc.setDigit(0,4,(byte)v,true);
    v = (int)( temp * 10 ) % 10;
    lc.setDigit(0,3,(byte)v,false);
    lc.setChar(0,2,' ',false);
    lc.setChar(0,1,'*',false);
    lc.setChar(0,0,'C',false);
    delay(1);
  } else if (mode<=50) {
  //  lc.clearDisplay(0);
    lc.setChar(0,7,'H',false);
    lc.setChar(0,6,' ',false);
    v = (int)( humidity / 10 ) % 10;
    lc.setDigit(0,5,(byte)v,false);
    v = (int)( humidity  ) % 10;
    lc.setDigit(0,4,(byte)v,true);
    v = (int)( humidity * 10 ) % 10;
    lc.setDigit(0,3,(byte)v,false);
    lc.setChar(0,2,' ',false);
    lc.setChar(0,1,'*',false);
    lc.setChar(0,0,'o',false);
    delay(1);
  } else if (mode<=75) {
  //  lc.clearDisplay(0);
    lc.setChar(0,7,'P',false);
    lc.setChar(0,6,' ',false);
    v = (int)( pressure_mm / 100 ) % 10;
    lc.setDigit(0,5,(byte)v,false);
    v = (int)( pressure_mm/10  ) % 10;
    lc.setDigit(0,4,(byte)v,false);
    v = (int)( pressure_mm ) % 10;
    lc.setDigit(0,3,(byte)v,false);
    lc.setChar(0,2,' ',false);
    lc.setChar(0,1,'n',false);
    lc.setChar(0,0,'n',false);
    delay(1);
  } else {
  //  lc.clearDisplay(0);
    lc.setChar(0,7,'L',false);
    lc.setChar(0,6,' ',false);
    v = (int)( light / 1000 ) % 10;
    lc.setDigit(0,5,(byte)v,false);
    v = (int)( light / 100  ) % 10;
    lc.setDigit(0,4,(byte)v,false);
    v = (int)( light / 10 ) % 10;
    lc.setDigit(0,3,(byte)v,false);
    v = (int)( light ) % 10;
    lc.setDigit(0,2,(byte)v,false);
    lc.setChar(0,1,' ',false);
    lc.setChar(0,0,' ',false);
    delay(1);
  }
}

void Z1_modbus_tcp_slave() {
  Mb.Run();

  DEV_ID = WEATHER_STATION_Z1;
  TEMPERATURE = temp*10;
  TEMPERATURE_DHT = temp_dht*10;
  TEMPERATURE_BMP = temp_bmp*10;
  HUMIDITY = humidity*10;
  PRESSURE_MM = pressure_mm;
  LIGHT = light;
  
}



Git-репозиторий с ПО контроллера Arduino: github.com/krotos139/sh1_arduino_weather_station_v1

Вывод

Умная погодная станция с интерфейсом Ethernet может быть легко реализована на arduino.
Себестоимость устройства при опытном производстве — ~1700 руб
Время на сборку — ~1 день
Функциональность фантастическая — может работать как самостоятельное устройство с питанием от POE и от стандартного arduino-вского источника питания, так и как smart-устройство — позволяя из браузера получать всю необходимую информацию. Для автоматизированной обработки информации устройство предоставляет информацию в виде XML документа и по протоколу modbus.