image

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

Принцип работы очень прост:

  • Человек отправляет сообщение боту в Telegram
  • Rasberry Pi получает новые сообщение через Telegram API
  • RPi открывает входную дверь

Для начала я расположил RPi в шкафу, где находятся роутер и модем, и подключил его к интернету. Затем собрал транзисторный ключ:

  • n-p-n транзистор кт315 (другого не было)
  • Резистор 10КОм
  • Пару проводов

Схема выглядит примерно так:

Схема ключа

Как собиралось

Как собиралось

Потом надо было написать код:

Код
package main
import (
  // Системные пакеты
  "log"
  "io/ioutil"
  "path/filepath"
  "strings"
  "time"

  // Парсер yaml файлов
  "gopkg.in/yaml.v2"

  // Библиетка для работы с rpi
  "github.com/stianeikeland/go-rpio"

  // Библитека для работы с telegram API
  "github.com/Syfaro/telegram-bot-api"
)

type Config struct {
  // Токен телеграм бота
  Token             string `yaml:"token"`
  // Разрешенные айдишники чатов
  AllowedChatIds    []int `yaml:"allowed_chat_ids"`
  // Ключевые слова для открывания двери
  OpenDoorPhrases   []string `yaml:"open_door_phrases"`
  TurnLedOnPhrases  []string `yaml:"turn_led_on_phrases"`
  TurnLedOffPhrases []string `yaml:"turn_led_off_phrases"`
}

var bot *tgbotapi.BotAPI
var config *Config
var OpenDoorPhrases []string
var TurnLedOnPhrases []string
var TurnLedOffPhrases []string
var AllowedChatIds []int
var doorOpened chan *tgbotapi.Message
var ledTurnedOn chan *tgbotapi.Message
var ledTurnedOff chan *tgbotapi.Message
var doorPin = rpio.Pin(10)
var ledPin = rpio.Pin(9)

func readConfig() (*Config, error) {
  var yamlFile []byte
  var err error
  filename, _ := filepath.Abs("./config.yml")
  yamlFile, err = ioutil.ReadFile(filename)
  if err != nil {
    return nil, err
  }
  var conf Config
  if err := yaml.Unmarshal(yamlFile, &conf); err != nil {
    return nil, err
  }
  return &conf, err
}

func main() {
  var err error
  // Читаем конфиг
  if config, err = readConfig(); err != nil {
    panic(err)
  }

  // Инициализируем бота
  bot, err = tgbotapi.NewBotAPI(config.Token)
  if err != nil {
    log.Panic(err)
  }

  // Для работы с gpio в rpi
  if err = rpio.Open(); err != nil {
    log.Panic(err)
  }
  defer rpio.Close()
  // Устанавливаем пины на output
  ledPin.Output()
  doorPin.Output()

  // Инициализируем все остальные переменные 
  doorOpened = make(chan *tgbotapi.Message)
  ledTurnedOn = make(chan *tgbotapi.Message)
  ledTurnedOff = make(chan *tgbotapi.Message)
  AllowedChatIds = config.AllowedChatIds
  OpenDoorPhrases = config.OpenDoorPhrases
  TurnLedOnPhrases = config.TurnLedOnPhrases
  TurnLedOffPhrases = config.TurnLedOffPhrases
  log.Printf("Authorized on account %s", bot.Self.UserName)

  var ucfg tgbotapi.UpdateConfig = tgbotapi.NewUpdate(0)
  ucfg.Timeout = 60
  err = bot.UpdatesChan(ucfg)

  // Слушаем события
  go Listen()
  ListenUpdates()
}

func OpenDoor() chan<- *tgbotapi.Message {
  // Открываем дверь
  go launchDoor()
  return (chan<-*tgbotapi.Message)(doorOpened)
} 

func TurnLedOn() chan<- *tgbotapi.Message {
  // Включаем светодиод
  ledPin.High()
  return (chan<-*tgbotapi.Message)(ledTurnedOn)
}

func TurnLedOff() chan<- *tgbotapi.Message {
  // Выключаем светодиод
  ledPin.Low()
  return (chan<-*tgbotapi.Message)(ledTurnedOff)
}

// Открывание двери
func launchDoor() {
  log.Println("door is beeing opened")
  doorPin.High()
  ledPin.High()
  time.Sleep(100*time.Millisecond)
  doorPin.Low()
  ledPin.Low()
}

// Проверяет, является ли указанное сообщение ключевым
func tryToDo(text string, phrases []string) bool {
  for i:=0; i<len(phrases); i++ {
    if strings.ToLower(text) == phrases[i] {
      return true
    }
  }
  return false
}

// Проверяет, является ли указанный чат разрешенным
func auth(chatId int) bool {
  for i:=0; i<len(AllowedChatIds); i++ {
    if chatId == AllowedChatIds[i] {
      return true
    }
  }
  return false
}

// Отправка сообщения
func send(chatId int, msg string) {
  log.Println(msg)
  bot_msg := tgbotapi.NewMessage(chatId, msg)
  bot.SendMessage(bot_msg)
}


func Listen() {
  for {
    select {
      case msg := <- doorOpened:
        reply := msg.From.FirstName + " открыл(а) дверь"
        send(msg.Chat.ID, reply)
      case msg := <- ledTurnedOn:
        reply := msg.From.FirstName + " включил(а) светодиод"
        send(msg.Chat.ID, reply)
      case msg := <- ledTurnedOff:
        reply := msg.From.FirstName + " выключил(а) светодиод"
        send(msg.Chat.ID, reply)
    }
  }
}

func ListenUpdates() {
  for {
    select {
    case update := <-bot.Updates:
      userName := update.Message.From.UserName
      chatID := update.Message.Chat.ID
      text := update.Message.Text
      // Проверяем является ли этот чат разрешенным
      if !auth(chatID) {
        reply := "Вам нельзя это делать"
        log.Println(reply)
        bot_msg := tgbotapi.NewMessage(chatID, reply)
        bot.SendMessage(bot_msg)
        continue
      }

      log.Printf("[%s] %d %s", userName, chatID, text)
      // По очереди пытаемся открыть дверь, включить/выключить светодиод
      if tryToDo(text, OpenDoorPhrases) {
        OpenDoor() <- &update.Message
      }
      if tryToDo(text, TurnLedOnPhrases) {
        TurnLedOn() <- &update.Message
      }
      if tryToDo(text, TurnLedOffPhrases) {
        TurnLedOff() <- &update.Message
      }
    }
  }
}


Что касается config.yml; Это просто конфиг:

token: <secret token>
allowed_chat_ids: 
  - <id1>
  - <id2>
open_door_phrases:
  - open
  - open the door
  - open door
  - door open
  - дверь откройся
  - открыть дверь
  - открыть
  - открыть
  - откройся
  - открой
  - сим-сим, откройся
  - abrete sesamo
  - -o
  - o
  - отк
  - /open
turn_led_on_phrases:
  - led on
  - test on
turn_led_off_phrases:
  - led off
  - test off

Оставалось только загрузить скомпилированный файл на RPi и подключить все провода.

Так выглядит готовый вариант устройства
Так выглядит готовый вариант устройства

Кнопка, к которой я подключил систему управления
Кнопка, к которой я подключил систему управления

Сама дверь
Сам замок

Чат с ботом

Комментарии (11)


  1. ibrin
    20.10.2015 13:19

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


  1. mcleod095
    20.10.2015 13:45

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


    1. kaikash
      20.10.2015 16:08

      Нет, там все это асинхронно запускается(разные горутины), получится, что дверь будет открыта на несколько секунд дольше
      К тому же, я не планирую малину оставлять там только для работы с дверью; в планах есть еще несколько интересных проектов, так что малина еще пригодится :)


      1. mcleod095
        20.10.2015 16:14

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


        1. kaikash
          20.10.2015 16:28

          Не стоит забывать то, что в офисе работает ±40 человек, и 100 запросов одновременно никто делать не будет. А если кто-то решит злоупотреблять, его просто кикнут из беседы telegram, и все :)


  1. IvanT
    20.10.2015 14:09
    +2

    Статья интересная, но вот решение странное. Достать брелок, приложить, положить в карман, войти — неудобно; достать телефон, разблокировать, найти иконку телеграма, запустить, найти чат замка, открыть, найти поле ввода, нажать, набрать open, отправить, заблокировать телефон, положить в карман — звучит куда удобнее :) Особенно удобно наверное открывать зимой когда руки в перчатках, а также тогда когда руки чем-то заняты. А если заняты зимой и в перчатках… :)


    1. ibrin
      20.10.2015 14:41

      А вдруг там налево-направо-по коридору прямо-вниз по лестнице-снова по коридору и дверь? И тут домофон звонит. -Wassuuuup! -Aaaaaaa -Открывай, чувак! И он такой не вставая хлабысь команду в чат и все открыл!


    1. alexpp
      20.10.2015 15:48

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


      1. IvanT
        20.10.2015 15:53

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


  1. zorgrhrd
    20.10.2015 15:13
    +1

    Не судите строго, это моя первая статья!

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


    1. egor_masalitin
      20.10.2015 17:23

      Согласен, также в правилах написания статей хабра, указано, что подобные фразы не приветствуются.