Доброго времени суток хабр-сообщество.
С момента моего последнего поста про умный дом прошло много времени. Я решил его делать начиная с погодной станции.
FT232RL USB To Serial Line Download Line Downloader USB TO 232 $ 4.47
Корпус пластиковый ~100 руб
Реализация устройства
Устройство было полностью собрано из покупных компонентов на базе Arduino. Все датчики были соединены проводами и расположены внутри покупного корпуса эля электроустройств. Прозрачные окна в опытном образце сделаны из прозрачной липкой ленты.
В устройстве предусмотрен LED-дисплей, который будет хорошо работать при отрицательных температурах, и позволяет использовать устройство по назначению без компьютера. Для удобства использования в ночное время — в ПО организована обратная связь по освещенности.
В качестве мозгов выбрана плата Iboard w5100, по причине того, что в ней уже присутствует весь функцонал arduino + Ethernet shield + sensor board для размножения питания и земли, при подключении большого количества устройств. Такая высокоинтегрированная плата съэкономит деньги и место. Также эта плата поддерживает работу с Passiv POE.
Передачу данных я решил сделать проводным (в отличие от большинства решений умного дома) по следующим причинам:
Провод в любом случае тянуть — будь то питания или передачи данных
В случае если решено запитать устройство от химических источников питания — то будет возникать вопрос их замены
Проводные коммуникации кажутся более надёжными:
К ним не будет претензий со стороны Роскомнадзора, так как ничего не излучают в эфир
Не возникнет проблем со взаимным влиянием других приемо-передатчиков в той-же частоты
Не возникнет огромного количества проблем связанных с реализацией радиосвязи (временного разделения эфира, затухания сигнала, влиение других таких-же систем при перекрывании зон приёма, и.т.д.)
Чтобы не плодить интерфейсы, и все сделать единообразно — я решил для передачи данных использовать 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 документ:
Стоит заметить, что для вывода букв — мне пришлось модернизировать таблицу символов в библиотеке 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;
}
Умная погодная станция с интерфейсом Ethernet может быть легко реализована на arduino.
Себестоимость устройства при опытном производстве — ~1700 руб
Время на сборку — ~1 день
Функциональность фантастическая — может работать как самостоятельное устройство с питанием от POE и от стандартного arduino-вского источника питания, так и как smart-устройство — позволяя из браузера получать всю необходимую информацию. Для автоматизированной обработки информации устройство предоставляет информацию в виде XML документа и по протоколу modbus.