[Из песочницы] Моя реализация системы домашней автоматизации

ea962ea6d63c4af99f2a68e426d88fa5.jpgДавно читаю статьи на Хабре о системах домашней автоматизации, захотелось описать то, над чем я работаю уже более 2 лет. Для лучшего понимания моей ситуации необходимо сделать небольшое вступление.

Три года назад мы с семьей переехали в новую трехкомнатную квартиру (67.5 кв.м), хотя технически квартира конечно старая — сталинка, дом 1946 года постройки. Алюминиевая двухпроводная проводка с кусками медного многожильного кабеля 1 кв.мм в некоторых местах. Ремонт предстоял капитальный, делать решил все сам, и начал с полной замены проводки. Было куплено 700м силового кабеля на освещение и розетки 1.5 и 2.5 кв.мм, бухта витой пары, немного коаксиала для телевизионных антенн (на всякий случай). Зачем так много и что из из этого вышло — прошу под кат.
Проводку я решил делать сразу правильную, а именно: без распредкоробок, от каждой точки кабель идет в щиток (за исключение розеток, которые могут быть в группе по 2-3 точки, кабель идет в щиток от крайней, остальный соединены шлейфом) — т.е. от каждого выключателя свой кабель в щиток, от каждой точки освещения — свой кабель в щиток, около розеток по паре точек rj-45 на всякий случай. Кабелей, конечно, получается много. Где-то он проложен в полу с соблюдением всех норм ПУЭ, как, например, в детской:

09e0717328cb46a0b0df95701ac88122.jpg

Где-то — по потолку, например вид на дверь спальни:

f0ff505103224720825920b62b9da86c.jpg

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

Вот так выглядел временный «щиток» в процессе экспериментов:

1e40bd1311d2412686170c0ae8619274.jpg

Жуткое зрелище, однако все работало и в таком виде этот монстр жил несколько месяцев. В один прекрасный день звезды удачно сложились и волевым решением щиток был переделан. Заняло это 3 дня на шаткой лестнице под потолком (а потолки в сталинке 3м), но оно того стоило. Вот как стал выглядеть щиток после этого:

34b2e41e94a54217bad80ce53d2e0e45.jpg

И общий вид:

054e1585eeb74b7ebe62e3fcd946e1a3.jpg

Это далеко не конечный вид щитка, не хватает еще УЗО, автоматов в 3 раза меньше, чем нужно, нет импульсных реле — зато есть неотключаемые линии, все кабели заведены на клеммы, есть хоть какая-то селективность автоматов.

Теперь, когда у Вас появилось примерное представление о том, с чем именно мне предстояло работать, можно приступать к описанию «мозгов» системы. Не мудрствуя лукаво, за основу я взял arduino, тем более что у меня уже лежали купленные задолго до этого 2 платы freeduino, 2 ethernet-шилда и один motor-шилд. Также заказал у китайцев несколько модулей реле по 4шт на каждом. И еще нашел по совету в одной из статей на Хабре в Оби выключатели, в которые можно было вставить специальные пружинки и они превращались в выключатели без фиксации. Настал черед писать код.

Вообще я по жизни тесно связан с компами. Закончил мех-мат, на чем только не писал — pascal, c#, c++, 1с, php, javascript — можно еще многго перечислять. Последнее время работаю в сфере веб-разработки, также имею дело с ip-телефонией. Поэтому придумать алгоритм — просто, а вот с электроникой «на вы» — простые вещи знаю и умею, а когда дело касается чего-то более сложного — микроконтроллеры, фьюзы, дребезг контактов — с этим сложнее. Но не боги горшки обжигают, глаза боятся, а руки делают. Решил делать все просто. Подаю на вход выключателя землю с ардуины, выходы с выключателя подключаю к аналоговым пинам на ардуине (хотя можно и к цифровым, это не важно). Соединяю пины ардуино с пинами на модуле реле. Технически — все готово, при нажатии на выключатель ардуино видит значение LOW на нужном пине и выставляет LOW на пине, соединенном с модулем реле. С учетом того, что все кабели от точек в квартире приходят на клеммы — подключение не заняло много времени.
Для борьбы с дребезгом контактов, после изучения темы в интернете, была выбрана библиотека Bounce2. Вообще изначально хотелось написать универсальный код, чтобы его можно было использовать хоть на 2 выключателя, хоть на 22. Именно эта задача легла в основу всего алгоритма. Ну, теперь от слов — к коду. Я не буду полностью выкладывать весь код, ссылки на гитхаб будут в конце статьи. Покажу только важные, с моей точки зрения, моменты.

Итак, объявление библиотек и переменных:

#include <Bounce2.h>
#include <EEPROM.h>

const byte cnt = 8;
const byte pin_cnt = 19;

int pins[] = {11,12,13,13,14,15,16,17};
int leds[] = {6, 3, 4, 5, 3, 4, 5, 3};

byte init_leds[cnt] ;
byte init_buttons[cnt];
int button_states[cnt];
Bounce debouncers[cnt];
unsigned long buttonPressTimeStamps[cnt];
boolean changed[cnt];


Смысл в чем: достаточно увеличить константу с количеством светильников/выключателей и прописать в массивы новые ассоциации — и все, новый светильник или выключатель будет добавлен. При этом, структура кода позволяет одним выключателем сразу управлять несколькими светильниками. Или несколькими выключателями сразу управлять одним светильником. Также для каждого выключателя создается свой собственный экземпляр объекта Bounce для антидребезга.

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

  for(byte i=0; i<cnt; i=i+1) {
    EEPROM.write(pins[i], 10);
  }

  for(byte i=0; i<cnt; i=i+1) {
    button_states[i] = 0;
    
    byte value = EEPROM.read(leds[i]);
    if(value==11) {
      init_leds[i] = LOW ;
    }else{
      init_leds[i] = HIGH ;
    }
    init_buttons[i] = HIGH;
    buttonPressTimeStamps[i] = 0;
    changed[i] = false;

    debouncers[i] = Bounce();

    pinMode(pins[i], INPUT);
    pinMode(leds[i], OUTPUT);
    
    digitalWrite(pins[i], init_buttons[i]);
    digitalWrite(leds[i], init_leds[i]);
    
    debouncers[i].attach( pins[i] );
    debouncers[i].interval(5);
  }


По поводу первого цикла пока промолчу, вернемся к нему чуть позже. А самое интересное начинается в теле основного цикла.

void loop(){
  for(byte i=0; i<cnt; i=i+1){
    byte dvalue = EEPROM.read(pins[i]);
    if(dvalue!=11) {
      changed[i] = debouncers[i].update();
      
      if ( changed[i] ) {
        int value = debouncers[i].read();
        if ( value == HIGH) {
         button_states[i] = 0;   
        } else {
           if (i > 0 and pins[i] == pins[i-1]) {
             byte prev_value = EEPROM.read(leds[i-1]);
                            
             if(prev_value == 11) {
               digitalWrite(leds[i], LOW );
               EEPROM.write(leds[i], 11);
             }else{
               digitalWrite(leds[i], HIGH);
               EEPROM.write(leds[i], 10);
             }
           } else {               
             byte value = EEPROM.read(leds[i]);
             if(value==11) {
               digitalWrite(leds[i], HIGH );
               EEPROM.write(leds[i], 10);
             }else{
               digitalWrite(leds[i], LOW);
               EEPROM.write(leds[i], 11);
             }
           }
                 
           button_states[i] = 1;
           buttonPressTimeStamps[i] = millis();     
        }
      }
   
      if ( button_states[i] == 1 ) {
        if ( millis() - buttonPressTimeStamps[i] >= 200 ) {
            button_states[i] = 2;
        }
      }
    }
  }

  delay( 10 );
} 


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

  for(byte i=0; i<cnt; i=i+1) {
    EEPROM.write(pins[i], 10);
  }

  ...


А проверяется вот тут:

  for(byte i=0; i<cnt; i=i+1){
    byte dvalue = EEPROM.read(pins[i]);
    if(dvalue!=11) {
    ...


Уже на этом этапе стали работать локальные выключтели на стенах. Но умный дом не был бы умным без интерфейса. Так как я имею дело с веб-разработкой, то и решено было делать веб-интерфейс. Тут-то и пригодились ethernet-шилды. К сожалению, я так и не смог найти исходники первых версий программ, использующих ethernet-шилды для удаленного управления. Попробую поискать в бекапах, возможно там они есть. Но смысл был примитивен до безобразия. Каждому контроллеру назначался свой IP-адрес. Поднимался веб-сервер на ардуине, который анализировал GET-запросы и в зависимости от номера порта включал-выключал соответствующий светильник. Примеров такого рода в интернете очень много. Для веб-интерфейса был собран сервер на материнской плате с встроенным Intel Atom, был установлен Ubuntu Server 14.02, уствновлен стандартный набор LAMP, там же написан простенький интерфейс. Все исходники также будут в конце статьи. На данный момент он выглядит вот так:

7ba5b2d4c95c48f0ac3873cef3c76b23.jpg

Как видно, один светильник на кухне включен, а выключатель, отвечающий за него — заблокирован. Управление мега-простое — просто нажимаем на нужный элемент и его состояние меняется.

И все было бы отлично, если бы не одно «но». Ethernet-шилды постоянно зависали. Нет, локальное управление с выключателей работало всегда как часы. Но удаленное управление с веб-интерфейса постоянно отваливалось. Если управление работало сутки — это было отлично. Но чаще шилд подвисал уже через несколько часов после перезагрузки. Что я только не делал — пробовал разобраться с watch-dog, но мои платы не поддерживали его. Я заказал в Китае и заменил шилды на другие — на en28j60 — стало лучше, но все равно они периодически зависали. Я добавил в контроллер модуль реле, завел питание плат ардуино через нормально-замкнутые контакты на нем и одна из плат ардуино с определнной периодичностью дергало реле и питание обрывалось, а потом само восстанавливалось — однако это тоже не всегда работало, да и в момент перезагрузки мигал весль включенный свет, пусть на пару секунд, но тем не менее. Вот как выглядел контроллер к этому моменту:

ab071deb30a94677a261c5378a809124.jpg

И тогда было принято решение совсем отказаться от ethernet-шилдов. Я начал искать другие возможности удаленного управления. Пробовал подключать ардуины непосредственно к серверу и слать команды через Serial.read()/Serial.print() — работало через раз, стабильности так и не удалось добиться из-за того, что плата перезагружалась каждый раз, когда к ней шло обращение из скрипта на сервере. Я много читал о таких ошибках, понял только что это связано с DTR, где-то писали что можно инициализировать порт с дургими флагами, приводили примеры, которые у меня так и не заработали. Через некоторое время мне попалась статья о том, что из программатора USBAsp можно сделать переходник USB-I2C. Я решил — а почему бы и нет? Заказал у китайцев пару таких программаторов — и стал ждать.

И вот неделю назад приехала моя посылка. Один программатор был перепрошит прошивкой i2c tiny usb и я снова сел переписывать код. Тут начали проявляться особенности протокола. Мастером был конечно же сервер, а все платы ардуино — слейвами.

Код должен был решать следующие задачи:

— сообщить состояние запрошенного порта;
— переключить состояние запрошенного порта;
— выключить или включить все светильники.

И я столкнулся с проблемой. Я могу использовать стандартные команды из набор i2c-tools для общения с платами ардуино. Это команда

i2cget -y <номер шины> <адрес>


и

i2cset -y <номер шины> <адрес> 0x00 <byte значение>


Вроде как, судя по манам, можно было передавать значение word, но у меня это не заработало, либо я где-то ошибался.

Проблема в том, что, во-первых, если мне нужно переключить состояние некоторого порта, то я должен передать сначала номер команды, отвечающей за эту операцию, а потом номер порта. Я даже пробовал это сделать, отправлять 2 команды, а в коде собирать их по-очереди — но это было некрасиво и нерационально.

Проблема номер два — ардуино не может что-то ответить в момент, когда сама получает данные. Есть два метода библиотеки Wire — onReceive(), когда данные получены от мастера и onRequest(), когда данные запрошены мастером.

Поэтому я сделал так: веб-сервер имеет 6 команд:

  • команда «1» — получить состояние всех светильников
  • команда «2» — получить состояние блокировки всех выключателей
  • команда «5» — включить весь свет
  • команда «6» — выключить весь свет


команда, формирующаяся следующим образом: десятичное число следующего вида ; например, 105 — переключить 5 порт, 213 — переключить блокировку 13 порта. Эта команда переводится в hex и передается на ардуину, которая производит обратные преобразования и понимает, что нужно делать.

Вот как это выглядит со стороны сервера:

  ...
  if ($action == 3)
     $val_hex = dechex(intval($port) + 100);
  else
     $val_hex = dechex(intval($port) + 200);
  
  exec("sudo i2cset -y 7 $addr 0x00 0x$val_hex", $output);
  ...


И со стороны ардуино:

void receiveEvent(int howMany) {
  byte bytes = Wire.available();
  int x = 0;
  for (byte i=1; i <= bytes; i=i+1) {
    x = Wire.read();
  }
  
  if (x == 1 or x == 2 or x == 5 or x == 6) {
    do_action(x, 0);
  } else {
    if ( x > 200) {
      do_action (4, x - 200);
    } else {
      do_action (3, x - 100);
    }
  }  
}


Выглядит достаточно примитивно, но работает. Вторая проблема решается следующим образом. Вот функция do_action:

void do_action(byte command, byte port) {
  byte value = 0;
  byte dvalue = 0;
  switch (command) {
    case 1:
      start_request = true;
      request_type = 1;
      current_port = 0;
      
      break;
    case 2:
      start_request = true;
      request_type = 2;
      current_port = 0;
      
      break;
    case 3:
      value = EEPROM.read(port);
      if(value==11) {
        digitalWrite(port, HIGH);
        EEPROM.write(port, 10);
      } else {
        digitalWrite(port, LOW);
        EEPROM.write(port, 11);
      }
      
      break;
    case 4:
      dvalue = EEPROM.read(port);
      if(dvalue==11) {
        EEPROM.write(port, 10);
      } else {
        EEPROM.write(port, 11);
      }
      
      break;
    case 5:
      for (byte i=0; i<cnt; i = i + 1) {
        digitalWrite(leds[i], LOW);
        EEPROM.write(leds[i], 11);
      }
      
      break;
    case 6:
      for (byte i=0; i<cnt; i = i + 1) {
        digitalWrite(leds[i], HIGH);
        EEPROM.write(leds[i], 10);
      }
      
      break;
    default:
      
    break;
  }
}


С командами 3-6 все понятно, а вот команды 1 или 2 можно описать более подробно. Сервер сначала отправляет нужную команду, и когда ардуино получает команду 1 или 2, происходит инициализация флагов:

  start_request = true;
  request_type = 1;
  current_port = 0;


И потом сервер начинает посылать запросы к ардуино столько раз, сколько портов хочет опросить. На стороне сервера:

function get_data($address, $action, $cnt) {
        exec("sudo i2cset -y 7 $address 0x00 0x0$action", $output);
        $tmp = @$output[0];
        while (strpos($tmp,"rror")!==false) {
                exec("sudo i2cset -y 7 $address 0x00 0x0$action", $output);
                $tmp = @$output[0];
        }
        $str = "";
        for ($i = 1; $i <= $cnt; $i++) {
                exec("sudo i2cget -y 7 $address", $output);
                $tmp = @$output[0];
                while (strpos($tmp,"rror")!==false) {
                        exec("sudo i2cget -y 7 $address", $output);
                        $tmp = @$output[0];
                }
                if ($tmp) {
                        if (strpos($tmp,"1")!==false)
                                $str .= "1";
                        else
                                $str .= "0";
                }
                unset($output);
                unset($tmp);
        }
                        
        return $str;
}

$str = array();
$c = 1;
while ($c <= $tryes) {
        $tmp = get_data($addr, $action, $cnt);
        
        if (strlen($tmp) == $cnt)
                $str[] = $tmp;
        
        $c++;
}

$new_array = array_count_values($str);

asort($new_array);

$res = "";
$max = 0;
foreach ($new_array AS $key=>$val) {
        if ($val >= $max) {
                $res = $key;
                $max = $val;
        }
}

return preg_split('//', $res, -1, PREG_SPLIT_NO_EMPTY);


В двух словах — мы делаем несколько ($tryes > 3) попыток опросить ардуино, получаем строку, состоящую из 0 или 1. Везде при любой команде мы смотрим на ответ, если там есть слово Error — значит при передаче были ошибки и нужно повторить передачу. Несколько попыток сделано для гарантии правильности переданной строки, массив сворачивается по строкам методом array_count_values($str); и в итоге мы получаем массив с количеством вхождений одинаковых строк, выдаем ту строку, которую мы больше всего раз получили от ардуино.

Со стороны ардуино все проще:

void requestEvent() {
  if (request_type == 1) {
    byte value = EEPROM.read(leds[current_port]);
    if(value==11) {
      Wire.write(1);
    } else {
      Wire.write(0);
    }
    
    current_port = current_port + 1;
  } else if (request_type == 2) {
    byte dvalue = EEPROM.read(pins[current_port]);
    if(dvalue==11) {
      Wire.write(1);
    } else {
      Wire.write(0);
    }

    current_port = current_port + 1;
  }
}


В коде веб-странички прописаны элементы управления:

<a class="lamp living_room" id="lamp0x4d3" rel='0x4d' onclick="lamp_click('0x4d',this.id, 3);" ></a>
<a class="lamp kitchen" id="lamp0x4d4" rel='0x4d' onclick="lamp_click('0x4d',this.id, 4);" ></a>
<a class="lamp children_main" id="lamp0x4d5" rel='0x4d' onclick="lamp_click('0x4d',this.id, 5);" ></a>
<a class="lamp children_second" id="lamp0x4d6" rel='0x4d' onclick="lamp_click('0x4d',this.id, 6);" ></a>
<a class="lamp sleeproom_main" id="lamp0x423" rel='0x42' onclick="lamp_click('0x42',this.id, 3);" ></a>
<a class="lamp sleeproom_lyuda" id="lamp0x424" rel='0x42' onclick="lamp_click('0x42',this.id, 4);" ></a>
<a class="lamp sleeproom_anton" id="lamp0x425" rel='0x42' onclick="lamp_click('0x42',this.id, 5);" ></a>

<a class="button button_living_room" id="button0x4d15" onclick="button_click('0x4d',this.id, 15);" ></a>
<a class="button button_kitchen" id="button0x4d14" onclick="button_click('0x4d',this.id, 14);" ></a>
<a class="button button_children_main" id="button0x4d16" onclick="button_click('0x4d',this.id, 16);" ></a>
<a class="button button_children_second" id="button0x4d17" onclick="button_click('0x4d',this.id, 17);" ></a>
<a class="button button_sleeproom_door1" id="button0x4212" onclick="button_click('0x42',this.id, 12);" ></a>
<a class="button button_sleeproom_door2" id="button0x4213" onclick="button_click('0x42',this.id, 13);" ></a>
<a class="button button_sleeproom_lyuda1" id="button0x4214" onclick="button_click('0x42',this.id, 14);" ></a>
<a class="button button_sleeproom_lyuda2" id="button0x4215" onclick="button_click('0x42',this.id, 15);" ></a>
<a class="button button_sleeproom_anton1" id="button0x4216" onclick="button_click('0x42',this.id, 16);" ></a>
<a class="button button_sleeproom_anton2" id="button0x4217" onclick="button_click('0x42',this.id, 17);" ></a>


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

Ах да, вот так сейчас выглядит контроллер:

05d20e6f0e8747bc940f202e2606fd2a.jpg

После того, как все было установлено на место, все завелось с первого раза. А на следующий день возник непонятный эффект — если включить все светильники в спальне, начинает моргать весь свет в спальне с периодичностью раз в 2-3 секунды. Я просидел всю ночь, ковыряя код, на тестовом стенде такого косяка не было, значит проблема не в коде. Перерыл кучу форумов, в тоге на одном из них нашел описание похожего симптома, проверил догадку — и проблема исчезла. А все дело было в том, что я питал все три ардуины от компьютерного блока питания, 12В, и старенькие freeduino спокойно кушали и не жужжали, а arduino uno v3 — не могла, и при включении всех релешек у нее стабилизатор питания нагревался так, что прикоснуться нельзя было. Уменьшил напряжение питания до 5В 2А — и все заработало как надо.

Планов громадье, надо заканчивать ремонт в квартире, на очереди коридор и ванная с туалетом, в мечтах управление водонагревателем и каждой розеткой, благо теперь на шину I2C можно повесить сколько угодно ардуин и каждая будет заниматься своим делом. Также в планах добавить импульсные реле на din-рейку, чтобы модули реле только управляли этими импульсными реле, а уже вся нагрузка шла через последних. Потому что есть серьезные сомнения в надежности китайских модулей реле. Но это все — в будущем.

Как и обещал, ссылки на гитхаб:

Веб-интерфейс
Скетчи для ардуино

© Geektimes