- Цена: $3.58
Дешевые GSM/GPRS модули подробно изучены энтузиастами, в том числе на mysku.ru. Создано немало сигнализаций, систем телеметрии и даже интеллектуальный почтовый ящик. Но во всех обзорах, которые я видел, использовался канальный режим передачи данных — либо телефонный звонок либо SMS. Я расскажу о своем опыте пакетной передачи данных, причем через протокол UDP в условиях жестких ограничений на трафик.
Все началось, когда товарищ установил себе в машину недорого купленный по случаю догреватель Webasto Thermo Top C, попутно доукомплектовав его циркуляционным насосом.
Протокол обмена идентифицировать не удалось, равно как и модель автомобиля, на котором он был изначально установлен. Но было экспериментально установлено, что при замыкании одного из пинов на +12 вольт питания отопитель оживал и начинал работать.
Сразу возник вопрос — как им управлять? Первой идеей, лежащей на поверхности, было приспособить 433 МГц радиобрелок, наподобие такого:
Что и было сделано. Практически сразу же обнаружились недостатки данного варианта:
1. Отсутствие шифрование радиообмена и отсутствие уникального идентификатора брелка. Конечно, вряд-ли кто-то специально будет слушать эфир, чтоб склонировать запрос на включение отопителя. Но в условиях города если кто-то рядом точно таким брелком будет включать дома торшер, включится и ваш отопитель, что однажды и произошло.
2. Циркуляционный насос питается от того же реле, что и запуск отопителя. Останавливается насос соответственно тоже в момент команды на остановку отопителя, а хотелось бы, чтоб он поработал еще несколько минут.
3. Недостаток, не связанный напрямую со способом управления отопителем, но требующий решения: отопитель высаживает основной аккумулятор. Увидеть, что двигатель теплый, но стартер уже не способен его провернуть — удовольствие ниже среднего.
4. И наконец, полное отсутствие какой-либо обратной связи. Если автомобиль запаркован в прямой видимости из окна квартиры, факт прохождения команды на включение еще можно отследить по появившемуся пару из выхлопной трубы отопителя. Но часто автомобиль остается на стоянке вне зоны видимости, и нет никакой уверенности, что мы придем именно к теплому автомобилю. Команда на включение может утонуть в городских помехах. Команда может дойти, но отопитель не включится из-за проблем в топливной системе или из-за низкого напряжения аккумулятора.
По итогам первого сезона эксплуатации началось обдумывание вариантов включения отопителя с возможностью обратной связи. Проводились научно-исследовательские и опытно-конструкторские работы с радиомодулями SX1278, результаты были многообещающие, но с моими технологическими возможностями габариты абонентского терминала приемлемыми никак не получались. Таскать на ключах набалдашник размером с Нокию 3310 исключительно ради управления вебастой? Было решено, что оно того не стоит.
Следующей идеей было использование мобильной сети для управления по интернету. Именно она и была реализована. Терминал для управления получался еще большего размера, но он все равно у каждого с собой — это смартфон. В минусы варианта можно записать наличие некоторой пусть небольшой, но абонплаты, в плюсы — расстояние, ограниченное только покрытием мобильной сети и возможность управлять из нескольких мест с нескольких устройств.
Было сформулировано техническое задание:
— отопитель и контроллер будут питаться от отдельной аккумуляторной батареи, благо у VW T4 для этого есть место под водительским сиденьем;
— контроллер измеряет три параметра в автомобиле: температуру теплоносителя и напряжение двух аккумуляторов;
— контроллер устанавливает с сервером соединение по UDP и раз в 10 секунд передает на сервер три измеренных параметра;
— сервер возвращает подтверждение приема, в нем же передается состояние реле отопителя;
— поскольку напряжения обеих батарей известны, бесплатным бонусом можно организовать развязку аккумуляторов: как только напряжение на основной батарее падает ниже установленного предела, реле физически разъединяет батареи;
— для управления основными исполнительными механизмами требуются 3 реле: включение отопителя, включение циркуляционного насоса охлаждающей жидкости, включение аккумуляторов в параллель;
— четвертую свободную релюшку на плате задействуем под физическое выключение-включение питания модема, предполагая, что в плане перезагрузки это надежнее, чем дергание специального пина;
— понадобится интернет-сервер с минимум двумя «белыми» портами: для связи с контроллером и для веб-интерфейса пользователя.
Список компонентов:
1. Arduino Pro Mini — 1 шт.
2. Плата реле 4-канальная — 1 шт.
3. Датчик DS18B20 — 1 шт.
4. Модем M590 — 1 шт.
5. Корпус, провода, изолента, припой, клеммы, USB-UART для программирования Arduino.
Поскольку были нарекания, что схемы, нарисованные во Fritzing, представляют из себя нагромождение проводов и сложны для восприятия, я разделил схему на две части — сигнальную и питающую. Схему подключения к реле исполнительных механизмов приводить не буду, так как реализация зависит от типа применяемых устройств. Достаточно знать, что реле №1 управляет отопителем, реле №2 — циркуляционным насосом, реле №3 — зарядкой дополнительного аккумулятора, реле №4 — перегружает модем.
Сигнальная часть:
Резистивные делители для измерения напряжений аккумуляторов подбираются из расчета, чтобы максимально возможное напряжение аккумулятора (ну скажем, 15 вольт) после деления не превышало 1.1 вольта — опорное напряжение внутреннего источника микроконтроллера. Я использовал 1.1 кОм в нижнем плече и 15 кОм в верхнем. Коэффициент для каждой конкретной пары резисторов нужно вписать в скетч перед его загрузкой (для правильной работы алгоритма развязки аккумуляторов) и в php-скрипт интерфейса пользователя (для правильного отображения напряжения батарей).
Размещать делители нужно по возможности поближе к ардуине, иначе длинные провода собирают помехи, и показания, и без того не сильно точные, начинают «прыгать».
Питание:
Контроллер собран в корпусе от коммутатора D-Link и помещен под водительское сиденье рядом с дополнительным аккумулятором.
Температурный датчик протянут в моторный отсек и прикручен синей изолентой к патрубку возврата охлаждающей жидкости к отопителю.
Фото, сделанное в процессе монтажа:
#include <SoftwareSerial.h>
#include <Regexp.h>
#include <OneWire.h>
SoftwareSerial mySerial(10,11);
OneWire ds(2);
String s;
int mode = 0;
unsigned long millis5;
unsigned long conntimeout;
unsigned long pumptimeout;
int u0, u0prev, u1;
byte data[2];
byte chargerelay = 0;
byte pumprelay = 0;
byte heaterrelay = 0;
void setup()
{
mySerial.begin(2400);
Serial.begin (115200);
pinMode(6, OUTPUT);
digitalWrite(6, HIGH);
pinMode(7, OUTPUT);
digitalWrite(7, HIGH);
pinMode(8, OUTPUT);
digitalWrite(8, HIGH);
pinMode(9, OUTPUT);
digitalWrite(9, HIGH);
pinMode(13, OUTPUT);
digitalWrite(13, LOW);
analogReference(INTERNAL);
u0 = analogRead(0);
u1 = analogRead(1);
ds.reset();
ds.write(0xCC);
ds.write(0x44);
millis5 = millis();
conntimeout = millis();
}
void loop() {
char c;
char buf [100] = "";
MatchState ms;
if (mySerial.available() > 0) {
c = mySerial.read();
Serial.print©;
s += c;
if ((c == 'n') || (c == '>')) {
s.toCharArray(buf, s.length());
ms.Target(buf);
if ( (mode == 6) && (ms.Match("UDPRECV:1,1,(%d)") > 0)){
if (ms.Match("UDPRECV:1,1,0") > 0) {
if (heaterrelay) pumptimeout = millis();
heaterrelay = 0;
digitalWrite(9, HIGH);
}
if (ms.Match("UDPRECV:1,1,1") > 0) {
heaterrelay = 1;
digitalWrite(9, LOW);
pumprelay = 1;
digitalWrite(8, LOW);
}
conntimeout = millis();
}
if ( (mode == 7) && (ms.Match("UDPSEND") > 0)){
mode = 6;
}
if ( (mode == 6) && (c == '>')) {
mode = 7;
millis5 = millis();
}
if ( (mode == 5) && (ms.Match("UDPSETUP:1,OK") > 0)) {
conntimeout = millis();
mode = 6;
millis5 = millis();
}
if ( (mode == 4) && (ms.Match("OK") > 0)) {
conntimeout = millis();
mode = 5;
millis5 = millis();
}
if ( (mode == 3) && (ms.Match("OK") > 0)) {
conntimeout = millis();
mode = 4;
millis5 = millis();
}
if ( (mode == 2) && (ms.Match("OK") > 0)) {
conntimeout = millis();
mode = 3;
millis5 = millis();
}
if ( (mode == 1) && (ms.Match("OK") > 0)) {
conntimeout = millis();
mode = 2;
millis5 = millis();
}
if ( (mode == 0) && ms.Match("PBREADY") > 0) {
conntimeout = millis();
mode = 1;
millis5 = millis();
}
s = "";
}
}
if (millis()-millis5 > 5000) {
u0prev = u0;
u0 = analogRead(0);
u1 = analogRead(1);
//Коэффициент резистивного делителя вычисляется исходя из имеющихся номиналов резисторов
if ((u0prev*0.0157 > 13.2) && (u0*0.0157 > 13.2)) {
chargerelay = 1;
digitalWrite(7, LOW);
}
if ((u0prev*0.0157 < 12.8) && (u0*0.0157 < 12.8)) {
chargerelay = 0;
digitalWrite(7, HIGH);
}
if (mode == 7) {
ds.reset();
ds.write(0xCC);
ds.write(0xBE);
data[0] = ds.read();
data[1] = ds.read();
ds.reset();
ds.write(0xCC);
ds.write(0x44);
char buf[10]="________#";
buf[0] = (data[0] & 0b00111111) + 0x30;
buf[1] = (((data[0] & 0b11000000) >> 6) | ((data[1] & 0b00001111) << 2)) + 0x30;
buf[2] = ((data[1] & 0b11110000) >> 4) + 0x30;
buf[3] = (char)(u0 & 0b0000000000111111) + 0x30;
buf[4] = (char)(((u0 & 0b0000001111000000) >> 6) | ((u1 & 0b0000000000000011) << 4)) + 0x30;
buf[5] = (char)((u1 & 0b0000000011111100) >> 2) + 0x30;
buf[6] = (char)((u1 & 0b0000001100000000) >> 8) + 0x30;
buf[7] = (char)(heaterrelay + pumprelay *2 + chargerelay * 4 + 0x30);
mySerial.print(buf);
}
if (mode == 6) {
mySerial.print("AT+UDPSEND=1,8r");
Serial.println("Sent AT+UDPSEND=1,8");
}
if (mode == 5) {
mySerial.print("AT+UDPSETUP=1,12.34.56.78,1234r");
Serial.println("Sent AT+UDPSETUP=1,12.34.56.78,1234");
}
if (mode == 4) {
mySerial.print("AT+XIIC=1r");
Serial.println("Sent AT+XIIC=1");
}
if (mode == 3) {
mySerial.print("AT+CGDCONT=1,"IP","my.operator.apn"r");
Serial.println("Sent AT+CGDCONT=1,"IP","my.operator.apn"");
}
if (mode == 2) {
mySerial.print("AT+XISP=0r");
Serial.println("Sent AT+XISP=0");
}
if (mode == 1) {
mySerial.print("ATE0r");
Serial.println("Sent ATE0");
}
//Помпа работает еще 5 минут после отключения подогревателя
if (pumprelay && !heaterrelay) {
if (millis() - pumptimeout > 300000) {
pumprelay = 0;
digitalWrite(8, HIGH);
}
}
millis5 = millis();
}
if (millis() - conntimeout > 120000) {
// 120 секунд не было принято ни одного байта. Дергаем питание модема и инициализируемся заново.
digitalWrite(6, LOW);
delay(1000);
digitalWrite(6, HIGH);
mode = 0;
conntimeout = millis();
}
}
Скорость обмена модема установлена на 2400 bps, используется программный UART (библиотека SoftSerial).
Скетч инициализирует модем, устанавливает соединение с сервером, затем каждые 10 секунд считывает температуру охлаждающей жидкости и напряжения аккумуляторов, после чего передает их на сервер.
Получив ответ от сервера единственный символ ответа, анализирует его. Если этот символ — единица, то включает отопитель и циркуляционный насос. Если этот символ — ноль, то выключает отопитель, через 5 минут выключает циркуляционный насос.
Если в течение 2 минут не получено ни одного ответа от сервера, считает, что модем повис и дергает его питание.
Если на протяжении 5 секунд напряжение основной батареи более 13,2 вольта, замыкает вместе основную и дополнительную батарею. Если на протяжении 5 секунд напряжение объединенной батареи менее 12,8 вольта, разъединяет батареи.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#define BUFLEN 512
#define PORT 1234
void diep(char *s)
{
perror(s);
exit(1);
}
int main(void)
{
struct sockaddr_in si_me, si_other;
int s, i, slen=sizeof(si_other);
char buf[BUFLEN];
char fbuf[BUFLEN]=" n";
FILE *flo, *fts, *fte, *fu0, *fu1, *fbi, *fco;
if ((s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP))==-1)
diep("socket");
memset((char *) &si_me, 0, sizeof(si_me));
si_me.sin_family = AF_INET;
si_me.sin_port = htons(PORT);
si_me.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(s, &si_me, sizeof(si_me))==-1)
diep("bind");
i = 1;
for (;;) {
if (recvfrom(s, buf, BUFLEN, 0, &si_other, &slen)==-1) diep("recvfrom()");
signed short temp = (buf[0] - 0x30) | ((buf[1] - 0x30) << 6) | ((buf[2] - 0x30) << 12);
// float temperature = temp * 0.0625;
int u0 = ((buf[3] - 0x30) | (((buf[4] - 0x30) & 0b00001111) << 6));
int u1 = ((((buf[4] - 0x30) & 0b00110000) >> 4) | ((buf[5] - 0x30) << 2) | ((buf[6] - 0x30) << 8 ));
int bits = buf[7] - 0x30;
time_t t = time(NULL);
struct tm *ptm = gmtime(&t);
sprintf(fbuf, "%04d%02d%02d%02d%02d%02d,%d,%d,%d,%d,%s:%dn",
ptm->tm_year+1900, ptm->tm_mon+1, ptm->tm_mday, ptm->tm_hour, ptm->tm_min, ptm->tm_sec,
temp, u0, u1, bits,
inet_ntoa(si_other.sin_addr), ntohs(si_other.sin_port));
flo = fopen("/var/log/udpser.log", "a");
fwrite(fbuf, 1, strlen(fbuf), flo);
fclose(flo);
sprintf(fbuf, "%d", t);
fts = fopen("/etc/udpser/ts.txt", "w");
fwrite(fbuf, 1, strlen(fbuf), fts);
fclose(fts);
fte = fopen("/etc/udpser/te.txt", "w");
sprintf(fbuf, "%d", temp);
fwrite(fbuf, 1, strlen(fbuf), fte);
fclose(fte);
fu0 = fopen("/etc/udpser/u0.txt", "w");
sprintf(fbuf, "%d", u0);
fwrite(fbuf, 1, strlen(fbuf), fu0);
fclose(fu0);
fu1 = fopen("/etc/udpser/u1.txt", "w");
sprintf(fbuf, "%d", u1);
fwrite(fbuf, 1, strlen(fbuf), fbi);
fclose(fu1);
fbi = fopen("/etc/udpser/bi.txt", "w");
sprintf(fbuf, "%d", bits);
fwrite(fbuf, 1, 1, fbi);
fclose(fbi);
fco = fopen("/etc/udpser/co.txt", "r");
fread(buf, 1, 1, fco);
fclose(fco);
if (sendto(s, buf, 1, 0, &si_other, slen)==-1) diep("sendto()");
i = i + 1;
}
close(s);
return 0;
Серверная часть для обработки UDP-подключения написана на языке C и работает на VPS под управлением Debian. Обязанность сервера — получив пакет, выделить из него фрагменты, соответствующие переданной температуре, напряжениям и состояниям реле, и разложить их по соответствующим файлам. Затем взять из файла состояние отопителя и передать его контроллеру в виде одного символа.
<?php
if ($_POST["q"] != "") {
file_put_contents('/etc/udpser/co.txt', $_POST["q"]);
echo "<head><meta http-equiv="Refresh" content="2" /></head>";
echo "<body>Запрос обработан</body>";
exit(0);
};
$lasttimestamp = file_get_contents("/etc/udpser/ts.txt");
echo "<head><meta http-equiv="Refresh" content="10" /></head>";
echo "<body>";
echo "<table border=0><tr align="left">";
echo "<th>Последнее обновление: </th><th>";
echo time()-intval($lasttimestamp)." сек. назад</th></tr>";
echo "<tr align="left"><th>Температура: </th><th>".number_format((float)file_get_contents("/etc/udpser/te.txt")*0.0625, 4, '.', '')." °С</th></tr>";
echo "<tr align="left"><th>Основной аккумулятор: </th><th>";
echo number_format((float)intval(file_get_contents("/etc/udpser/u0.txt"))*0.01552, 2, '.', '')." вольт</th></tr>";
echo "<tr align="left"><th>Доп. аккумулятор: </th><th>";
echo number_format((float)intval(file_get_contents("/etc/udpser/u1.txt"))*0.01560, 2, '.', '')." вольт</th></tr>";
$bits = intval(file_get_contents("/etc/udpser/bi.txt"));
echo "<tr align="left"><th>Нагреватель: </th>";
if ($bits & 1) {
echo "<th>ВКЛ</th>";
} else {
echo "<th>ВЫКЛ</th>";
}
echo "</tr>";
echo "<tr align="left"><th>Насос: ";
if ($bits & 2) {
echo "<th>ВКЛ</th>";
} else {
echo "<th>ВЫКЛ</th>";
}
echo "</tr>";
echo "<tr align="left"><th>Зарядка: ";
if ($bits & 4) {
echo "<th>ВКЛ</th>";
} else {
echo "<th>ВЫКЛ</form></th>";
}
echo "</tr></table>
";
$command = intval(file_get_contents("/etc/udpser/co.txt"));
if (!$command) {
echo "<form method=post><input type=hidden name="q" value=1><input type=submit value="Запросить включение нагревателя"></form>";
} else {
echo "<form method=post><input type=hidden name="q" value=0><input type=submit value="Запросить выключение нагревателя"></form>";
}
echo "</body>";
// echo "<input type="button" value="Refresh Page" onClick="window.location.reload()"";
?>
Интерфейс пользователя написан на языке PHP и располагается в домашнем каталоге веб-сервера. Цель скрипта — вытащить из файлов «сырые» значения температуры, напряжений и состояний, привести их к «человекочитаемому» виду и отобразить. При нажатии на кнопку запуска отопителя положить единичку в файл состояния отопителя. При нажатии на кнопку остановки отопителя положить нолик в файл состояния отопителя.
Получилось конечно брутальненько:
Но я не хипстер, всяческим цветным кнопкам и прочим AJAXам не обучен.
В таком виде устройство эксплуатируется уже около месяца. Никаких фатальных недостатков за это время не обнаружено. Включает-выключает, температуру-напряжения показывает. Обнаружился полезный побочный эффект — с утра на холодной машине можно узнать фактическую температуру за окном прямо со смартфона, не подходя к оконному термометру.
Немного о расходе трафика.
Каждые 10 секунд передается запрос и принимается ответ. В запросе 28 байт заголовок и 7 байт полезной нагрузки. В ответе 28 байт заголовок и 2 байта полезной нагрузки. Итого 65 байт каждые 10 секунд. 390 байт в минуту, 561 600 байт в сутки, 17 409 600 байт за 31 день. Расчеты практически совпадают с данными оператора:
На момент написания статьи на неполные 13 дней месяца было израсходовано 5.5 мегабайт из включенных в абонплату 50 (тариф «Легко сказать» МТС Беларусь) из теоретических 7 мегабайт.
Разницу списываю на нахождение автомобиля вне зоны покрытия сотовой связи.
Возможные доработки и улучшения.
Без особых трудозатрат контроллер можно дополнить GPS-модулем для слежения за перемещением. Можно использовать дополнительные реле для управления автомобильным оборудованием — мигать поворотниками, включать вентилятор печки и т.п. — у кого какие потребности и фантазия.
Для еще большей экономии трафика можно попытаться реализовать механизм KeepAlive — слать пакеты раз в несколько минут только для контроля состояния соединения.
Наконец, можно вскрыть родной протокол управления отопителем и снимать данные температуры по K-Line/CAN прямо со встроенного датчика, избавившись от костыля в виде внешнего DS18B20.