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

от автора

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


Рисунок 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.

ссылка на оригинал статьи http://habrahabr.ru/post/214011/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *