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

  • Цена: $3.58
  • Дешевые GSM/GPRS модули подробно изучены энтузиастами, в том числе на mysku.ru. Создано немало сигнализаций, систем телеметрии и даже интеллектуальный почтовый ящик. Но во всех обзорах, которые я видел, использовался канальный режим передачи данных — либо телефонный звонок либо SMS. Я расскажу о своем опыте пакетной передачи данных, причем через протокол UDP в условиях жестких ограничений на трафик.

    Все началось, когда товарищ установил себе в машину недорого купленный по случаю догреватель Webasto Thermo Top C, попутно доукомплектовав его циркуляционным насосом.

    Протокол обмена идентифицировать не удалось, равно как и модель автомобиля, на котором он был изначально установлен. Но было экспериментально установлено, что при замыкании одного из пинов на +12 вольт питания отопитель оживал и начинал работать.

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

    Сразу возник вопрос — как им управлять? Первой идеей, лежащей на поверхности, было приспособить 433 МГц радиобрелок, наподобие такого:

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

    Что и было сделано. Практически сразу же обнаружились недостатки данного варианта:

    1. Отсутствие шифрование радиообмена и отсутствие уникального идентификатора брелка. Конечно, вряд-ли кто-то специально будет слушать эфир, чтоб склонировать запрос на включение отопителя. Но в условиях города если кто-то рядом точно таким брелком будет включать дома торшер, включится и ваш отопитель, что однажды и произошло.

    2. Циркуляционный насос питается от того же реле, что и запуск отопителя. Останавливается насос соответственно тоже в момент команды на остановку отопителя, а хотелось бы, чтоб он поработал еще несколько минут.

    3. Недостаток, не связанный напрямую со способом управления отопителем, но требующий решения: отопитель высаживает основной аккумулятор. Увидеть, что двигатель теплый, но стартер уже не способен его провернуть — удовольствие ниже среднего.

    4. И наконец, полное отсутствие какой-либо обратной связи. Если автомобиль запаркован в прямой видимости из окна квартиры, факт прохождения команды на включение еще можно отследить по появившемуся пару из выхлопной трубы отопителя. Но часто автомобиль остается на стоянке вне зоны видимости, и нет никакой уверенности, что мы придем именно к теплому автомобилю. Команда на включение может утонуть в городских помехах. Команда может дойти, но отопитель не включится из-за проблем в топливной системе или из-за низкого напряжения аккумулятора.

    По итогам первого сезона эксплуатации началось обдумывание вариантов включения отопителя с возможностью обратной связи. Проводились научно-исследовательские и опытно-конструкторские работы с радиомодулями SX1278, результаты были многообещающие, но с моими технологическими возможностями габариты абонентского терминала приемлемыми никак не получались. Таскать на ключах набалдашник размером с Нокию 3310 исключительно ради управления вебастой? Было решено, что оно того не стоит.

    Следующей идеей было использование мобильной сети для управления по интернету. Именно она и была реализована. Терминал для управления получался еще большего размера, но он все равно у каждого с собой — это смартфон. В минусы варианта можно записать наличие некоторой пусть небольшой, но абонплаты, в плюсы — расстояние, ограниченное только покрытием мобильной сети и возможность управлять из нескольких мест с нескольких устройств.

    Было сформулировано техническое задание:

    — отопитель и контроллер будут питаться от отдельной аккумуляторной батареи, благо у VW T4 для этого есть место под водительским сиденьем;

    — контроллер измеряет три параметра в автомобиле: температуру теплоносителя и напряжение двух аккумуляторов;

    — контроллер устанавливает с сервером соединение по UDP и раз в 10 секунд передает на сервер три измеренных параметра;

    — сервер возвращает подтверждение приема, в нем же передается состояние реле отопителя;

    — поскольку напряжения обеих батарей известны, бесплатным бонусом можно организовать развязку аккумуляторов: как только напряжение на основной батарее падает ниже установленного предела, реле физически разъединяет батареи;

    — для управления основными исполнительными механизмами требуются 3 реле: включение отопителя, включение циркуляционного насоса охлаждающей жидкости, включение аккумуляторов в параллель;

    — четвертую свободную релюшку на плате задействуем под физическое выключение-включение питания модема, предполагая, что в плане перезагрузки это надежнее, чем дергание специального пина;

    — понадобится интернет-сервер с минимум двумя «белыми» портами: для связи с контроллером и для веб-интерфейса пользователя.

    Список компонентов:

    1. Arduino Pro Mini — 1 шт.

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

    2. Плата реле 4-канальная — 1 шт.

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

    3. Датчик DS18B20 — 1 шт.

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

    4. Модем M590 — 1 шт.

    5. Корпус, провода, изолента, припой, клеммы, USB-UART для программирования Arduino.

    Поскольку были нарекания, что схемы, нарисованные во Fritzing, представляют из себя нагромождение проводов и сложны для восприятия, я разделил схему на две части — сигнальную и питающую. Схему подключения к реле исполнительных механизмов приводить не буду, так как реализация зависит от типа применяемых устройств. Достаточно знать, что реле №1 управляет отопителем, реле №2 — циркуляционным насосом, реле №3 — зарядкой дополнительного аккумулятора, реле №4 — перегружает модем.

    Сигнальная часть:

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

    Резистивные делители для измерения напряжений аккумуляторов подбираются из расчета, чтобы максимально возможное напряжение аккумулятора (ну скажем, 15 вольт) после деления не превышало 1.1 вольта — опорное напряжение внутреннего источника микроконтроллера. Я использовал 1.1 кОм в нижнем плече и 15 кОм в верхнем. Коэффициент для каждой конкретной пары резисторов нужно вписать в скетч перед его загрузкой (для правильной работы алгоритма развязки аккумуляторов) и в php-скрипт интерфейса пользователя (для правильного отображения напряжения батарей).

    Размещать делители нужно по возможности поближе к ардуине, иначе длинные провода собирают помехи, и показания, и без того не сильно точные, начинают «прыгать».

    Питание:

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

    Контроллер собран в корпусе от коммутатора D-Link и помещен под водительское сиденье рядом с дополнительным аккумулятором.

    Температурный датчик протянут в моторный отсек и прикручен синей изолентой к патрубку возврата охлаждающей жидкости к отопителю.

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

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

    Скетч для 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 и располагается в домашнем каталоге веб-сервера. Цель скрипта — вытащить из файлов «сырые» значения температуры, напряжений и состояний, привести их к «человекочитаемому» виду и отобразить. При нажатии на кнопку запуска отопителя положить единичку в файл состояния отопителя. При нажатии на кнопку остановки отопителя положить нолик в файл состояния отопителя.

    Получилось конечно брутальненько:

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

    Но я не хипстер, всяческим цветным кнопкам и прочим AJAXам не обучен.

    В таком виде устройство эксплуатируется уже около месяца. Никаких фатальных недостатков за это время не обнаружено. Включает-выключает, температуру-напряжения показывает. Обнаружился полезный побочный эффект — с утра на холодной машине можно узнать фактическую температуру за окном прямо со смартфона, не подходя к оконному термометру.

    Немного о расходе трафика.

    Каждые 10 секунд передается запрос и принимается ответ. В запросе 28 байт заголовок и 7 байт полезной нагрузки. В ответе 28 байт заголовок и 2 байта полезной нагрузки. Итого 65 байт каждые 10 секунд. 390 байт в минуту, 561 600 байт в сутки, 17 409 600 байт за 31 день. Расчеты практически совпадают с данными оператора:

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

    На момент написания статьи на неполные 13 дней месяца было израсходовано 5.5 мегабайт из включенных в абонплату 50 (тариф «Легко сказать» МТС Беларусь) из теоретических 7 мегабайт.

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

    Разницу списываю на нахождение автомобиля вне зоны покрытия сотовой связи.

    Возможные доработки и улучшения.

    Без особых трудозатрат контроллер можно дополнить GPS-модулем для слежения за перемещением. Можно использовать дополнительные реле для управления автомобильным оборудованием — мигать поворотниками, включать вентилятор печки и т.п. — у кого какие потребности и фантазия.

    Для еще большей экономии трафика можно попытаться реализовать механизм KeepAlive — слать пакеты раз в несколько минут только для контроля состояния соединения.

    Наконец, можно вскрыть родной протокол управления отопителем и снимать данные температуры по K-Line/CAN прямо со встроенного датчика, избавившись от костыля в виде внешнего DS18B20.

Оцените статью