Как стать автором
Обновить

Мой интернет вещей: Гостевой замок

Время на прочтение 17 мин
Количество просмотров 79K
Так сложились обстоятельства, что имеется у меня однокомнатная квартира, в которой я не живу, а сдавать ее «обычным способом» мне не интересно. Попробовал я ее сдать через сервис Airbnb, понравилось. И не то, что бы это выгоднее, но точно интереснее, процесс захватывает. Но я не об этом…

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

Хотелось что-то высокотехнологичное. Мыслил я так: смартфоны сегодня есть у многих, а если рассматривать моих потенциальных гостей, то почти у всех. Неужели ничего не придумано для iPhone и Android, чтобы со смартфона открывать дверной замок? Оказывается, очень даже придумано. Нашел два интересных решения.

1. Lockitron Bolt
Подробно о гаджете
Физически — это накладка-сервопривод на обычный дверной замок, управляемая по Bluetooth. Дополнительно имеется модуль Bridge, с одной стороны подключаемый к Интернет, а другой стороны — управляющийся по беспроводному каналу сервоприводом. Управляется это все через приложение для смартфонов и через веб-морду. Можно отправлять электронные ключи друзьям, что бы они сами вошли, можно открыть для ни них дверь через Интернет.

Цена вопроса $99 за сервопривод + $79 за Bridge + доставка.

В целом гаджет интересный, но есть пара проблем. Во-первых, он еще не готов. Принимаются предварительные заказы на бета-версию. Во-вторых, сервопривод рассчитан на отпирание и запирание замка в пол-оборота, именно такие замки популярны на его родине в США. У меня же в квартире цилиндровый замок европейского типа, для открытия/закрытия нужно сделать минимум 1 полный оборот цилиндра. Можно, конечно, врезать в дверь дополнительный замок, но см. «во-первых».

2. Kevo Smart Lock
Подробно о гаджете
Это самодостаточное устройство, замок со встроенным сервоприводом, Bluetooth-интерфейсом и сенсорным датчиком. Выглядит более привлекательно. В качестве «ключа» используется приложение для смартфона, специальный брелок или обычный ключ. Принцип работы похож на штатную сигнализацию некоторых современных автомобилей. Если в зоне действия беспроводной связи есть нужный телефон или брелок, то достаточно только прикоснуться к замку для открытия. Очень эффектно.

Цена вопроса $202 + доставка.

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

Выяснилось, что тяжелые металлические двери толщиной 60 мм в человеческих жилищах в США не очень популярны, вследствие чего и умные замки для таких дверей тоже не очень популярны. «Американский» тип замка устанавливается в дверь толщиной до 55 мм, как правило, деревянную.

Это все было обоснование проекта. Теперь собственно сама разработка.

Немного о себе. Работаю в сфере ИТ, но с контроллерами моя работа никак не связана. Имею базовое образование по робототехнике, по специальности не работал, но не все из ВУЗовского обучения успел забыть. Архитектура «изделия» у меня в голове нарисовалась сразу, без моего вербального участия.

Выглядела она так:

Командный сервер <=> Контроллер <=> Исполнительный механизм

Проект я начал без конкретного плана, без функциональных требований, как исследовательский. Небольшое гугление выявило, что мне нужна платформа Arduino. Далее был почти месяц изучения возможностей платформы, различных примеров реализованных проектов и краткий курс вспоминания навыков программирования на C++.

Параллельно я думал над выбором исполнительного устройства. Во всех найденных в Сети примерах цилиндр замка жестко соединялся с сервоприводом, и меня это категорически не устраивало, т.к. нужно было, чтобы замок можно было открыть снаружи ключом, а жесткое соединение такую возможность исключало. Ну и опять же для открытия/закрытия моего замка нужен минимум один полный оборот цилиндра, а сервоприводы обычно поворачиваются только на 180°. Были мысли сделать сложный сервопривод, состоящий из приводного мотора, сцепления и датчика угла, но я ее отверг в следствии отсутствия металлообрабатывающего оборудования, которое непременно потребовалось бы для реализации этой задачи.

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

Наткнулся на электромеханический замок CISA стоимостью в 35 т.р. Можно было поменять свой замок (того же производителя) на него, благо размеры стандартные, но цена…

Еще один параллельный процесс — выработка функциональных требований к системе. Тут и далее будем называть систему «Гостевой замок». Итак:
  1. Гостевой замок не является основным замком, обеспечивающим безопасность квартиры;
  2. Гостевой замок используется только для того, что бы гости могли войти первый раз в квартиру в отсутствии хозяина и могли в отсутствии хозяина последний раз из нее выйти, и при этом не требовалось передавать ключ через почтовый ящик или каким-либо иным небезопасным способом;
  3. Остальное время квартира запирается на обычные замки с обычными металлическими ключами, соответственно Гостевой замок может быть в активном и неактивном состоянии;
  4. Гостевой замок должен быть безопасен с точки зрения пожарной безопасности, не препятствовать эвакуации при отключенном электропитании;
  5. Хозяин должен иметь возможность дистанционно через Интернет открыть Гостевой замок, перевести его в активное или неактивное состояние;
  6. Гость может воспользоваться «гостевым ключом», полученным от хозяина, для открытия замка;
  7. «Гостевой ключ» срабатывает только в непосредственной близости от замка;
  8. «Гостевой ключ» ограничен по времени действия.

Предстояло сделать мучительный выбор контроллера между Arduino Ethernet и Arduino Yun. Казалось бы, Yun — самое то, что доктор прописал. Поднимаем на линуксовой части веб-сервер, реализуем на нем всю логику высокого порядка и в нужный момент отдаем команды на микроконтроллерную часть. Остается только купить у провайдера настоящий IP-адрес и все заработает. Но что-то меня останавливало от такого выбора. Сначала это были тревожные сомнения о том, что в квартиру нужно провести настоящий IP, а потом я понял, что на самом деле меня останавливает масштабируемость. Один замок на Yun заработает на «раз-два», а два замка? А если у двух замков разные хозяева и гости? И т.д.

Таким образом, окончательный выбор пал на Arduino Ethernet.

Тем временем решился вопрос с исполнительным механизмом. В качестве такового был выбран электроригельный замок YLI для систем контроля и управления доступом, ценой чуть более 3 т.р. Такие стоят в офисах, на складах и т.п. помещениях, как правило в комплекте с кучей другого СКУД-оборудования.

Питание 12В, максимальный пусковой ток соленоида 900 мА. Особая фишка для пользователей металлических дверей: его можно встроить в дверную коробку, ширина 37 мм (коробки обычно 40-50 мм).

Такие замки бывают двух типов, открытые без питания (NO) и закрытые без питания (NC). В помещениях для людей настоятельно рекомендуется использовать только тип NO, что бы в нештатной ситуации, сопровождающейся отключением электропитания, находящиеся внутри помещения люди без проблем смогли покинуть опасную зону.

От замка идет 5 проводов:
Красный: +12В
Черный: Земля
Оранжевый: Открытие. На нем обычно +4,8 В. Для того, что бы открыть замок нужно замкнуть провод на землю.
Зеленый и Белый: Датчик закрытия двери. Когда дверь открыта, провода разомкнуты, когда закрыта — замкнуты.

Итак, соберем первый прототип


Управлять 12 В цепью питания замка будем через полевой транзистор. Замыкать на землю провод открывания будем через N-P-N транзистор. Для красоты поставим двухцветный светодиод, что бы светил зеленым, когда замок не активен и красным, когда активен. Для придания драматизма всей сцене поставим пьезопищалку. Для ручного открытия изнутри, как на дверях с домофоном, поставим тактовую кнопку.

Скетч принимает команды по UART, распознает их и умеет активировать, деактивировать и открывать замок. Кроме того отвечает на запросы о текущем статусе замка (активен или нет) и состоянии двери (открыта или закрыта)

image

Скетч
#include <EEPROM.h>

// Цифровые и аналоговые входы/выходы
#define BEEPER  5  // Пищалка
#define BUTTON  2  // Кнопка
#define LED_RED 6  // Светодиод активированного замка
#define LED_GREEN  3 // Светодиод деактивированного замка
#define LOCK_POWER  7 // Затвор полевого транзистора, управляющего цепью питания замка
#define LOCK_OPEN  9 // Вход транзисторного ключа, открывающего замок
#define NO_PIN  8 // Датчик открытой двери

// Константы событий для основного цикла loop()
#define EVENT_NONE  0  // Ничего не происходит
#define EVENT_BUTTON  1 // Нажата кнопка
#define EVENT_ACTIVATE  2 // Получена команда активации замка
#define EVENT_DEACTIVATE  3 // Получена команда деактивации замка
#define EVENT_OPEN  4 // Получена команда открыть замок
#define EVENT_SERIAL  99 // Получена строка через UART

char deviceID[12];

int eventButton = EVENT_NONE;
int lockActive = LOW;

String inputString = "";

void pressButton(){ // Обработчик прерывания нажатия кнопки
  eventButton = EVENT_BUTTON;
}

void setup(){
  pinMode(BEEPER, OUTPUT);
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  pinMode(LOCK_POWER, OUTPUT);
  pinMode(LOCK_OPEN, OUTPUT);
  pinMode(BUTTON, INPUT);
  pinMode(NO_PIN, INPUT);
  Serial.begin(9600);


  inputString.reserve(30);
  attachInterrupt(0, pressButton, RISING);
 
  eventButton = EVENT_NONE;
  lockActive = EEPROM.read(0); // Считываем настройку активации замка "по умолчанию"
 
  digitalWrite(LOCK_POWER, lockActive);
  digitalWrite(LED_RED, lockActive);
  digitalWrite(LED_GREEN, !lockActive);
}

int commandProcessor(String incomingString){
  incomingString.trim();
  incomingString.toUpperCase();
  if (incomingString == "OPEN") {
    return EVENT_OPEN;
  }
  else if (incomingString == "ACTIVATE") {
    return EVENT_ACTIVATE;
  }
  else if (incomingString == "DEACTIVATE") {
    return EVENT_DEACTIVATE;
  }
  else {
    return EVENT_NONE;
  }
}

void loop(){

  // Предварительный обработчик событий от интерфейсов

  if (eventButton == EVENT_SERIAL){
    // сначала обработка только команд от Serial
    inputString.trim();
    inputString.toUpperCase();
    if (inputString == "STATUS") { //Запрос статуса замка
      if (lockActive == HIGH) {
        Serial.println("ACTIVE");
      }
      else {
        Serial.println("NOTACTIVE");
      }
    }
    else if (inputString == "DOOR") { // Запрос состояния двери по датчику
      if (digitalRead(NO_PIN) == HIGH) {
        Serial.println("CLOSE");
      }
      else {
        Serial.println("OPEN");
      }
    }
    else if (inputString == "NORMAL OPEN") { // Настройка начального состояния замка
      if (EEPROM.read(0) != LOW) {
        EEPROM.write(0, LOW);
        delay(10);
      }
    }
    else if (inputString == "NORMAL CLOSE") {
      if (EEPROM.read(0) != HIGH) {
        EEPROM.write(0, HIGH);
        delay(10);
      }
    }

    // Затем общие команды
    eventButton = commandProcessor(inputString);

    inputString = "";
  }

  // Основной обработчик событий

  if (eventButton == EVENT_BUTTON) {
    eventButton = EVENT_OPEN; // что делать при нажатой кнопке решаем здесь и идем дальше
  }
  else if (eventButton == EVENT_ACTIVATE) {
    lockActive = HIGH;
    digitalWrite(LOCK_POWER, HIGH);
    digitalWrite(LED_RED, HIGH);
    digitalWrite(LED_GREEN, LOW);
    tone(BEEPER, 700, 50);
  }
  else if (eventButton == EVENT_DEACTIVATE) {
    lockActive = LOW;
    digitalWrite(LOCK_POWER, LOW);
    digitalWrite(LED_RED, LOW);
    digitalWrite(LED_GREEN, HIGH);
    tone(BEEPER, 700, 50);
  }  
  else if (eventButton == EVENT_OPEN) {
    digitalWrite(LOCK_OPEN, HIGH);
    if (lockActive == HIGH) {
      tone(BEEPER, 750, 50);
    }
    delay(10);
    digitalWrite(LOCK_OPEN, LOW);
    eventButton = EVENT_NONE;
  }

  eventButton = EVENT_NONE;
}

void serialEvent() {
  while (Serial.available()) {
    char inChar = (char)Serial.read();
    inputString += inChar;
    if (inChar == '\n') {
      eventButton = EVENT_SERIAL;
    }
  }
}


Выводы по первой итерации проекта.
  1. Arduino — это Весчь!
  2. Инженерные навыки не пропьешь, оказывается, я много чего помню;
  3. Ethernet Shield резервирует много выходов Arduino Uno, если будет не хватать на светодиоды, придется экономить. Например, затвор полевого транзистора и красный светодиод можно подключить к одному пину, кнопку открытия не обязательно заводить в контроллер, можно сразу на замок или транзисторный ключ.


Добавим немного распределенности


В результатах гугления по запросу «управление arduino интернет» в основном рассказывается, как сделать простой веб-сервер для индикации показаний датчиков, или как на Yun построить селфи-будку для кота.

На Хабре был интересный обзор облачного сервиса Ninja Blocks. Похоже, что когда я читал этот обзор, сервис работал как-то не так.

Тот же автор ivizil совсем недавно опубликовал способ управления через HTTP-запросы. Я этот способ опробовал примерно за месяц до этой публикации и отверг его по соображениям эстетичности. Бомбить хостинг каждые 5-10 секунд одним и тем же запросом в ожидании одной единственной команды в сутки как-то не эстетично.

Идеальный вариант мне виделся в использовании XMPP. Клиент на Arduino подключается к серверу и получает от него команды: «включись», «выключись», «откройся» или запросы «состояние», «дверь». Гость тоже подключается как XMPP клиент, сервер разграничивает доступ и контролирует кто, какую и куда команду может отправить.

Увы, никаких готовых библиотек для реализации XMPP клиента на Arduino не нашлось. Будем довольствоваться старым добрым Telnet.

Второй прототип


В целях экономии цифровых выходов попробовал посадить затвор полевого транзистора и светодиод на один выход. Замку резко стало не хватать напряжения для вытягивания ригеля. Замеры показали, что на замке вместо необходимых 12В чуть менее 11В. Проблема в том, что когда на затворе 5В, между истоком и землей 11,8В, а когда 4,8В — 10,9В. Придется перед затвором поставить биполярный транзистор в цепь 12В, ну и учесть нюанс, что этот транзистор будет немного инвертировать сигнал.

Для второго прототипа нам потребуется Telnet-сервер. Написать его можно много на чем, я решил это сделать на Python. Признаюсь, мое знакомство с Python весьма шапочное. Я читал, что на нем очень быстро и удобно работать, что он кросплатформенен и даже смог с первой попытки написать «Hello World!».

Начал гуглить примеры работы с Telnet на Python и довольно быстро наткнулся на библиотеку Twisted. За основу взял пример простого чат-сервера и доработал его до «командного сервера».

Сценарий работы выглядит так:

При включении контроллер пытается подключиться к серверу. При неудачной попытке следующая повторяется через некий интервал времени. допустим 10 секунд.

При удачном соединении первым делом проверяем, а наш ли это сервер. Отправляем на него случайно сгенерированный код, сервер добавляет к нему секретное слово, хранящееся в таблице mySQL, вычисляет MD5 и отправляет обратно. Контроллер сравнивает полученный MD5 с аналогичным образом рассчитанным самостоятельно и если они совпадают соединение считается достоверным и мы готовы принимать команды от сервера, в ознаменование чего мы зажжем оранжевый светодиод. Синтаксис команд такой же, как для UART.

На сервере мы помечаем клиента, подключившегося таким способом как «замок».

«Ключи» подключаются похожим способом. После соединения клиент отправляет на сервер запрос на то, чтобы стать «ключом». Та же запросная авторизация: сервер отвечает случайной последовательностью символов, к ней надо добавить секретное слово, которое ключ должен знать, вычислить MD5 и отправить обратно. Если результат, отправленный клиентом совпадет с результатом, рассчитанным сервером, клиент определяется как «ключ» и ему разрешается отправлять команды «замкам».

В обработчике событий протокола парсим полученные от клиентов строки и пересылаем туда-сюда команды.

Код сервера на Python
from twisted.protocols import basic
from twisted.internet import reactor
from twisted.internet.protocol import ServerFactory 
from twisted.protocols.basic import LineOnlyReceiver
from twisted.application import service, internet
import hashlib 
import threading
import MySQLdb
from MySQLdb import Error
import random


class ChatProtocol(LineOnlyReceiver): 

    name = ""
    isKey = False
    authKey = ""
    
    def getSecret(self,isKey):
        if not self.factory.conn:
            self.factory.mySQLdbConnect()
        if isKey:
            reqTable = "lockkeys"
        else:
            reqTable = "locks"
        try:
            cursor = self.factory.conn.cursor()
            cursor.execute("SELECT `secret` FROM `" + reqTable + "` WHERE id='" + self.getName() + "'")
            rows = cursor.fetchall()
                        
            if cursor.rowcount > 0:
                cursor.close()
                return rows[0][0].strip() 
            else:
                cursor.close()
                return None
            
        except Error as e:
            print(e)
            return None

        finally:
            cursor.close()
            
    def validKey(self,lockID):
        if not self.isKey:
            return False
        if self.transport.getPeer().host == "127.0.0.1":
            return True
        if not self.factory.conn:
            self.factory.mySQLdbConnect()
        try:
            cursor = self.factory.conn.cursor()
            cursor.execute("SELECT `rights` FROM `hosts` WHERE (`lock`='" + lockID + "') AND (`lockkey`='"+ self.getName() + "')")
            rows = cursor.fetchall()
            
            if cursor.rowcount > 0:
                cursor.close()
                return rows[0][0]
            else:
                cursor.close()
                return False
        except Error as e:
            print(e)
            return False

        finally:
            cursor.close()            

    def getName(self): 
        if self.name!="": 
            return self.name 
        return self.transport.getPeer().host 

    def connectionMade(self): 
        print "New connection from "+self.getName()
        if self.transport.getPeer().host == "127.0.0.1":
            self.isKey = True 
        d = {self.getName() : self}
        self.factory.clients.update(d)

    def connectionLost(self, reason): 
        print "Lost connection from "+self.getName() 
        self.factory.clients.pop(self.getName()) 
        self.factory.sendMessageToAllclients(self.getName()+" has disconnected.") 
        

    def lineReceived(self, line): 
        #print self.getName()+" said "+line
        #if line[:3] == "/OK"
        if line[:5]=="/KEY:":
            str1 = '123456789'
            str2 = 'qwertyuiopasdfghjklzxcvbnm'
            str3 = str2.upper()
            str4 = str1+str2+str3
            ls = list(str4)
            random.shuffle(ls)
            self.authKey = ''.join([random.choice(ls) for x in range(12)])
            self.sendLine("ANSW:"+self.authKey)
            oldName = self.getName() 
            self.name = line[5:].strip() 
            self.factory.clients.pop(oldName)
            self.factory.clients.update({self.getName() : self})
            self.isKey = False
            print oldName+" has requested to be Key ID:"+self.getName()
        elif line=="/EXIT": 
            self.transport.loseConnection()
        elif line[:6]=="/ANSW:":
            secret = self.getSecret(True)
            if secret:
                m = hashlib.md5()
                m.update(self.authKey + secret)
                if m.hexdigest() == line[6:].strip():
                    self.isKey = True
                    print self.getName() + " was authorisated as Key"
                else:
                    self.sendLine("Authorisation fail")
                    self.transport.loseConnection()             
            else:
                self.sendLine("Authorisation fail")
                self.transport.loseConnection()


                
        if self.isKey: #Only from keys
            
            if line[:6]=="/OPEN:":
                if self.validKey(line[6:]):
                    adresat = self.factory.clients.get(line[6:],None)
                    if adresat:
                        adresat.sendLine("OPEN:" + self.getName() +  "\r\n")
                    else:
                        self.sendLine("DeviceID="+ line[6:] + " is not online\r\n")
            elif line[:10]=="/ACTIVATE:":
                if self.validKey(line[10:]):
                    adresat = self.factory.clients.get(line[10:],None)
                    if adresat:
                        adresat.sendLine("ACTIVATE:" + self.getName() +  "\r\n")
                    else:
                        self.sendLine("DeviceID="+ line[10:] + " is not online\r\n")
            elif line[:12]=="/DEACTIVATE:":
                if self.validKey(line[12:]):
                    adresat = self.factory.clients.get(line[12:],None)
                    if adresat:
                        adresat.sendLine("DEACTIVATE:" + self.getName() +  "\r\n")
                    else:
                        self.sendLine("DeviceID="+ line[12:] + " is not online\r\n")           
        else: # Only from locks
            if line[:4]=="/ID:": 
                oldName = self.getName() 
                self.name = line[4:].strip() 
                self.factory.clients.pop(oldName)
                self.factory.clients.update({self.getName() : self})
                print oldName+" changed name to "+self.getName() 
            elif line[:4]=="/RE:":
                secret = self.getSecret(self.isKey)
                if secret == None:
                    self.sendLine("Your Device ID is not register in Command Server")
                    self.transport.loseConnection()
                else:
                    requestCode = line[4:]
                    m = hashlib.md5()
                    m.update(requestCode + secret)
                    self.sendLine("AUTH:" + m.hexdigest() + "\r\n")
            elif line[:4]=="/OK:":
                adresat = self.factory.clients.get(line[4:],None)
                if adresat:
                    adresat.sendLine("OK:" + self.getName())
            else: 
                self.factory.sendMessageToAllclients(self.getName()+" says "+line)
    
    def sendLine(self, line): 
        self.transport.write(line+"\r\n") 
        
        

class ChatProtocolFactory(ServerFactory): 

    protocol = ChatProtocol 
    clients = {} 
    
    def mySQLdbConnect(self):
        try:
            self.conn = MySQLdb.connect(host='127.0.0.1',user='guestlock',passwd='passw',db='guestlock')
        except Error as e:
            print(e)
            return False
        else:
            return True
            
    def __init__(self):
        print "Starting server..." 
        self.clients = {}
        if self.mySQLdbConnect():
            print "Server ready!"
        else:
            print "Data base error. Server doesn't work correctly."
        

    def sendMessageToAllclients(self, mesg): 
        for client in self.clients.values():
            client.sendLine(mesg) 


factory = ChatProtocolFactory()
application = service.Application("CommandServer")
internet.TCPServer(12345, factory).setServiceParent(application)


Сервер следует запустить как демон с помощью штатной утилиты демонизации twistd
$twistd -y CommandServer.py


Скетч
#include <EEPROM.h>
#include <SPI.h>
#include <Ethernet.h>
#include <MD5.h>

// Цифровые и аналоговые входы/выходы
#define BEEPER  5  // Пищалка
#define BUTTON  2  // Кнопка
#define LOCK_POWER 7  // Замок активирован 
#define LED_DEACTIVE  6 // Замок деактивирован  
#define LOCK_OPEN  9  // Открытие замка
#define LED_ETHERNET  8 // Индикатор подключения к серверу
#define NO_PIN  A0  //Датчик NO замка АНАЛОГОВЫЙ


// Константы событий для основного цикла loop()
#define EVENT_NONE  0
#define EVENT_ETHERNET 98
#define EVENT_SERIAL  99


#define RECONNECT_TIME  5000 // Интервал попыток подключения к серверу, мс


char deviceID[12];
byte mac[] = { 
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
EthernetClient client;
IPAddress server(192,168,1,7);
long port = 12345;

unsigned long lastConnectionTime = 0;       
unsigned long disconnectTime = 0;
boolean lastConnected = false;                

volatile int eventButton = EVENT_NONE;  // Индикатор события для основного цикла loop()
int lockActive = LOW;

String inputString = "";        
String ethernetString = "";

long requestCode;

void pressButton(){ // Обработчик прерывания нажатия кнопки
  lockOpen();
}

void majorBeep() {
  tone(BEEPER, 750, 50);
}

void minorBeep() {
  tone(BEEPER, 300, 100);
}


void lockActivate(boolean lockState){
  digitalWrite(LOCK_POWER, lockState);
  digitalWrite(LED_DEACTIVE, !lockState);
  lockActive = lockState;
  majorBeep();
}

void lockOpen() {
  digitalWrite(LOCK_OPEN, HIGH);
  delay(50);
  digitalWrite(LOCK_OPEN, LOW);
  majorBeep();
}

void lockSetDefaultState(int state){
  if (EEPROM.read(0) != state) { 
    EEPROM.write(0, state); 
    delay(10);
  }
  majorBeep();
}

boolean serverAuthRequest() {
  if (!client.connected()){
    return false;
  }
  client.print("/ID:");
  client.println(deviceID);
  delay(1000);
  char rCode[6];
  ultoa(random(99999),rCode,16);

  unsigned char* hash=MD5::make_hash(rCode);
  char *md5str = MD5::make_digest(hash, 16);

  client.print("/RE:");
  client.println(md5str);

  String answerHash = md5str;
  answerHash.trim();
  for (int i=0; EEPROM.read(50+i) != 0; i++){
    answerHash += char(EEPROM.read(50+i));
  } 

  char *aCode = (char*)malloc(answerHash.length()+1);
  ;
  answerHash.toCharArray(aCode,answerHash.length()+1);

  hash=MD5::make_hash(aCode);
  md5str = MD5::make_digest(hash, 16);

  answerHash = md5str;

  free(md5str);
  free(aCode);

  unsigned long mm = millis();
  ethernetString = "";
  while (millis()-mm < RECONNECT_TIME) {
    if (client.available()) {
      char inChar = client.read();
      ethernetString += inChar;
      if (inChar == '\n') {
        ethernetString.trim();
        if (ethernetString.startsWith("AUTH:")) {

          ethernetString = ethernetString.substring(5);
          break;
        } 
        else {
          ethernetString = "";
        }
      } 
    }
  }
  return (ethernetString == answerHash);
}


boolean serverConnect() {
  if (client.connect(server, port)) {
    lastConnectionTime = millis();
    if (serverAuthRequest()) {
      Serial.println("Server autenfication sucsesseful");
      majorBeep();
      digitalWrite(LED_ETHERNET, HIGH);
      ethernetString = "";
      return true;
    } 
    else{
      Serial.println("Server autenfication fail");
      minorBeep();
      digitalWrite(LED_ETHERNET, LOW);
      ethernetString = "";
      client.stop();
      return false;
    }
  }   
  else {
    Serial.println("Connection fail");
    client.stop();
    digitalWrite(LED_ETHERNET, LOW);
    disconnectTime = millis();
    ethernetString = "";
    return false;
  }
}


void setup(){
  pinMode(BEEPER, OUTPUT);
  pinMode(LOCK_OPEN, OUTPUT);
  pinMode(BUTTON, INPUT);
  pinMode(LOCK_POWER, OUTPUT);
  pinMode(LED_DEACTIVE, OUTPUT);
  pinMode(LED_ETHERNET, OUTPUT);


  lockActivate(EEPROM.read(0));

  Serial.begin(9600);

  randomSeed(analogRead(A1));

  for (int i=1; i<12; i++) {
    deviceID[i-1] = EEPROM.read(i);
  }

  Serial.print("Device ID: "); 
  Serial.println(deviceID); 

  delay(1000);
  // start the Ethernet connection using a DNS server:
  Ethernet.begin(mac);

  Serial.print("My IP address: ");
  Serial.println(Ethernet.localIP());

  if (!serverConnect()) {
    disconnectTime = millis();
  }

  inputString.reserve(30);
  attachInterrupt(0, pressButton, RISING);
  eventButton = EVENT_NONE;
}

boolean commandProcessor(String incomingString, int commandSource){
  
  boolean result = true;
  String report = "OK";
  incomingString.trim();
  incomingString.toUpperCase();
  if (incomingString.startsWith("OPEN")) {
    lockOpen();
  } 
  else if (incomingString.startsWith("ACTIVATE")) {
    lockActivate(HIGH);
  } 
  else if (incomingString.startsWith("DEACTIVATE")) {
    lockActivate(LOW);
  } 
  else if (incomingString.startsWith("STATUS")) {
    if (lockActive == HIGH) {
      report = "ACTIVE";
    } 
    else {
      report = "NOTACTIVE";
    }    
  }
  else if (incomingString.startsWith("DOOR")) {
    if (analogRead(NO_PIN) > 1000) {
      report = "CLOSE";
    } 
    else {
      report = "OPEN";
    }
  }
  else {
    result = false;
  }

  if (result && (commandSource == EVENT_ETHERNET) && client.connected()) {
    if (incomingString.indexOf(":") > 0) {
      client.print("/" + report + incomingString.substring(incomingString.indexOf(":")));
    } 
    else {
      client.println("/" + report);
    }    
  }
  
  if (result && (commandSource == EVENT_SERIAL)) {
    Serial.println(report);
  }

}



void ethernetEvent() {
  if (client.available()) {
    char inChar = client.read();
    ethernetString += inChar;
    if (inChar == '\n') {
      eventButton = EVENT_ETHERNET;
    } 
  }
}


void loop(){

  if (eventButton == EVENT_ETHERNET) {
    commandProcessor(ethernetString, eventButton);
    ethernetString = "";
    eventButton = EVENT_NONE;
  }

  if (eventButton == EVENT_SERIAL) {
    commandProcessor(inputString, eventButton);
    inputString = "";
    eventButton = EVENT_NONE;
  }

  if (!client.connected() && lastConnected) {
    Serial.println("Disconnecting.");
    minorBeep();
    digitalWrite(LED_ETHERNET, LOW);
    disconnectTime = millis();
    client.stop();
  }

  lastConnected = client.connected();

  if ((!lastConnected) && (millis() > disconnectTime+RECONNECT_TIME)) {
    disconnectTime = millis();
    serverConnect();
  }

  ethernetEvent();
}

void serialEvent() {
  while (Serial.available()) {
    char inChar = (char)Serial.read();
    inputString += inChar;
    if (inChar == '\n') {
      eventButton = EVENT_SERIAL;
    } 
  }
}


ProtoShield


В аппаратной части контроллер видится мне логически завершенным, если что и захочу добавить, так это пару светодиодов для индикации каких-либо важных или не очень событий или состояний, а значит, самое время собрать его по-человечески. Сейчас это все выглядит как художественная инсталляция под названием «Какой провод резать?!!».

image

Закупаем Arduino ProtoShield за 240 руб. Паяльник, флюс и припой есть.

image

Шилд замка

Выводы по второй итерации:
  1. Python штука мощная, но непривычная;
  2. Паять я не разучился;
  3. Сервер будет работать на Rapsberry Pi;
  4. Надо заказать у провайдера реальный IP-адрес;
  5. Можно устанавливать «изделие» на «объект» и принимать гостей.

Теперь мы готовы к инсталляции системы. Как минимум открыть гостю дверь, находясь на работе, я уже смогу. Нерешенным остался вопрос управления со смартфонов гостей.

Продолжение следует...
Теги:
Хабы:
+27
Комментарии 58
Комментарии Комментарии 58

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн