GPRS-модем Neoway M590 "на колесах". Управление дополнительным отопителем в автомобиле.

Опубликовал | 14.12.2018

Дешевые 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 и помещен под водительское сиденье рядом с дополнительным аккумулятором.
Температурный датчик протянут в моторный отсек и прикручен синей изолентой к патрубку возврата охлаждающей жидкости к отопителю.

Фото, сделанное в процессе монтажа:

Скетч для Arduino
#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 вольта, разъединяет батареи.

Исходный код UDP-сервера
#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.

Планирую купить +3 Добавить в избранное +3 +3

(c) 2017 Источник материала

Рекламные ссылки