Управление микроконтроллером через telegram-бот с обратной связью

Задача

Нужно управлять различными устройствами: свет, вентиляция, полив, а также получать нужные данные от микроконтроллера.
При этом для учебно-тренировочных или DIY-задач совершенно не хочется задействовать дополнительные устройства, на которых будет размещаться сервер и уж тем более не оплачивать внешний статический ip-адрес.

Идея

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

Веб-сервер на Flask и бота будем размещать на ReplIt. Как это сделать бесплатно с работой 24/7 описано в статье Как хостить телеграм-бота

Первая попытка была использовать Arduino + Ethernet-модуль W5500, эта связка заработала только внутри локальной сети и провалилась при переносе на ReplIt, т.к. ссылка веб-сервера оказалась доступна только по протоколу https, который Arduino не поддерживает.

Решение нашлось в виде платы NodeMCU v3 с WiFi-модулем

NodeMCU v3 с WiFi-модулемNodeMCU v3 с WiFi-модулем

Эта плата, основанная на микроконтроллере ESP8266 программируется через среду Arduino IDE с небольшой настройкой.

Реализация — шаг 0 — настраиваем IDE

Пример будет для версии Arduino IDE 2.0.3, которая легко доступна на официальном сайте

d90c15c92a5035f93236c224b13fe619.png

После установки IDE необходимо добавить в нее библиотеки для работы с ESP8266.
Нужно зайти в настройки File→Preference

01db04a1462f6ed012ce1543b5f953b7.png

И добавить ссылки для скачивания информации о дополнительных платах:
http://arduino.esp8266.com/stable/package_esp8266com_index.json
https://dl.espressif.com/dl/package_esp32_index.json

c0e9b59297d1a8364f2ad558084e2f55.png

Далее открываем в левом меню Board manager и устанавливаем пакет для работы с ESP8266

cbe0b3e1db0a2e7ea67fb7aed0fb2642.png

После этого IDE готова к работе. Но прежде, чем загрузить код в ESP, нужно создать два скрипта на Python и получить ссылку, по которой будет осуществляться обмен данными.

Реализация — шаг 1 — Пишем код телеграм-бота и веб-сервера на flask

В данной статья будет описан принцип обмена данными, поэтому от ESP будет передаваться только «условная температура» (условная, потому-то всегда статичная из переменной) и состояния встроенного в микроконтроллер светодиода.

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

Создаем проект на ReplIt, в нем делаем два файла main.py (в нем будет телеграм-бот) и background.py (в нем будет веб-сервер). Более подробно процесс описан в статье Как хостить телеграм-бота

Telegram-бот

Размещаем в файле main.py.

Моменты, на которые стоит обратить внимание:

  • import pip pip.main(['install', 'pytelegrambotapi']) — установит необходимую нам библиотеку

  • f.write(str(call.from_user.id)) — записываем id пользователя, с которым общаемся. В данной реализации предполагаем, что с ботом не будет одновременно работать несколько человек.

  • bot.send_message(call.from_user.id,"Свет скоро включится") — отправляем сообщение пользователю, что «команда принята». Чуть позже, когда от ESP придет информация, что свет включен, мы отправим еще одно подтверждение. Именно для этого и нужно запоминать user_id

  • Токен от бота прячем в SECRETS, т.к. проект в бесплатном режиме открыт в режиме просмотра для всех. Подробнее об этом в документации

import os
from background import keep_alive
import pip
pip.main(['install', 'pytelegrambotapi'])
import telebot
import time

#очищаем все файлы или создаем пустые
with open('messages.txt','w') as f:
      f.write("")
with open('user_id.txt','w') as f:
      f.write("")
with open('from_esp.txt','w') as f:
      f.write("")
with open('from_tg.txt','w') as f:
      f.write("")

def get_last_update(now,last):
  # функция для определения времени получения данных от микроконтроллера
  diff = now-last
  if diff<60:
    return f"{int(diff)} сек назад"
  elif diff<60*60:
    return f"{int(diff/60)} мин назад"
  elif diff<60*60*24:
    return f"{int(diff/60/24)} ч назад"  
  else:
    return "Более дня назад"


bot = telebot.TeleBot(os.environ['TOKEN'])# Создаем бот

# Создание клавиатур, для удобной коммуникации с пользователем
start_keyboard = telebot.types.InlineKeyboardMarkup()
start_keyboard.add(
    telebot.types.InlineKeyboardButton('Получить информацию', callback_data='info'),
    telebot.types.InlineKeyboardButton('Управлять устройством', callback_data='control')
)

control_keyboard = telebot.types.InlineKeyboardMarkup()
control_keyboard.add(
    telebot.types.InlineKeyboardButton('Включить', callback_data='on'),
    telebot.types.InlineKeyboardButton('Выключить', callback_data='off')
)
# Клавиатуры будут прикреплены к сообщениям бота

# На любое сообщение пользователя присылаем варианты действий
# Как вариант, обрабатывать команду /start от пользователя
@bot.message_handler(content_types=['text'])
def get_text_message(message):
  bot.send_message(message.from_user.id,"Что вы хотите сделать?",reply_markup=start_keyboard)

#Обработка нажатий на кнопки 
@bot.callback_query_handler(func=lambda call: True)
def func(call):
  bot.answer_callback_query(call.id) # подтверждаем боту, что действие по кнопке выполнено
  with open('user_id.txt','w') as f:
      f.write(str(call.from_user.id)) # записываем в файл user_id. Он понадобится для отправки сообщений
  if call.data=='info':#нажата кнопка с callback_data='info', получаем информацию из файла
    with open("from_esp.txt","r") as f:# читаем файл
      temp,light,time_last = f.readlines()[0].split(';')#получаем значения переменных
      last_update = get_last_update(time.time(),float(time_last))# и время получения данных
    #отправляем сообщение пользователю
    bot.send_message(call.from_user.id,f"Все хорошо, \nТемпература: {temp} \nОсвещение: {'включено' if light=='1' else 'выключено'}\nОбновлено: {last_update}", reply_markup=start_keyboard)
  if call.data=='control':#нажата кнопка с callback_data='control'
    bot.send_message(call.from_user.id,"Вот что можно сделать:",reply_markup=control_keyboard)
  if call.data=='on':#нажата кнопка с callback_data='on'
    bot.send_message(call.from_user.id,"Свет скоро включится")#отправляем сообщение пользователю
    with open('from_tg.txt','w') as f:#записываем в файл действие, которое хотим сделать
      f.write('1')#включить свет
  if call.data=='off':
    bot.send_message(call.from_user.id,"Свет скоро выключится")
    with open('from_tg.txt','w') as f:
      f.write('0')#выключить свет
  
keep_alive()# запуск веб-сервера из файла background.py
bot.polling(non_stop=True, interval=0)# запуск телеграм-бота

Веб-сервер на Flask

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

Размещаем код в файле background.py
Этот сервер выполняет сразу 2 задачи:

  • Обеспечивает обмен данными с микроконтроллером через GET-запросы

  • Используется для поддержки работоспособности скрипта через UpTimeRobot. Подробности все в той же статье

from flask import Flask
from flask import request
from threading import Thread
import time
import requests

app = Flask('')

@app.route('/')#Создаем "главную страницу" которую будет пинговать UpTimeRobot
def home():
  return "I'm alive"

@app.route('/iot', methods=['GET'])#создает ссылку /iot на которую будут приходить запросы 
def iot():
  temp = request.args.get('temp') #получаем параметры из GET-запроса
  light = request.args.get('light')
  with open('from_esp.txt', 'r') as f:#читаем данные, полученные из ESP в прошлый раз
    old_temp,old_light,time_last = f.readlines()[0].split(';')
    if old_light=="0" and light=="1":# и если старое состояние выкл, а новое вкл
      with open('messages.txt', 'w') as f_m:# записываем в файл messages текст сообщения
        f_m.write("Свет включился")
    if old_light=="1" and light=="0":
      with open('messages.txt', 'w') as f_m:
        f_m.write("Свет выключился")
  with open('from_esp.txt', 'w') as f:#записываем в файл новые значения
    f.write(f"{temp};{light};{time.time()}")
    
  with open('from_tg.txt', 'r') as f:# читаем из файла действие, сделанное командой в телеграм боте
    new_state = f.read(1) #т.к. у нас только 1 параметр Включить/выключить свет, читаем 1 символ
  return new_state #возвращаем значение
#Для Flask-сервера это означает, что прочитанный символ будет показан на веб-странице https://сайт/iot


def run(): #функция запуска flask-сервера
  app.run(host='0.0.0.0', port=80)

def reminder():
  while True:
    with open('user_id.txt','r') as f:#пытаемся прочитать user_id. Номер чата с пользователем
      lines = f.readlines()
      if len(lines)>0:
        chat_id = lines[0]
      else:
        chat_id = None
    with open('messages.txt','r') as f:# читаем файл с сообщением
      lines = f.readlines()
      if len(lines)>0 and chat_id is not None:#если есть user_id и сообщение
        text = lines[0]
        token = os.environ['TOKEN']   
        requests.get(r"https://api.telegram.org/bot"
                         +token
                         +r"/sendMessage?chat_id="+chat_id
                         +r"&text="+text)
        #отправляем сообщение по специальной ссылке с использованием токена
    with open('messages.txt','w') as f:
      f.write("")#очищаем файл с сообщениями
    time.sleep(0.3)

def keep_alive():# запускаем flask и reminder в отдельных потоках
  t = Thread(target=run)
  t.start()
  tr = Thread(target=reminder)
  tr.start()

Прекрасно! Теперь, когда все запустилось, нужно записать ссылку доступа к серверу и ключ шифрования для доступа по протоколу https

Реализация — шаг 2 — код для ESP8266

После запуска сервера в правом верхнем углу экрана будет ссылка. Копируем ее и вставляем в браузер

0ce65b495044ace18f4ca9aae1f0defe.png

Нажимаем на иконку замка рядом с адресом сайта

11a61b890246c2d2db2295a35e785faa.png

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

38465b16b001a4eed769391155e507fe.png

Копируем и сохраняем «отпечаток SHA-1»

4826690d9cf65d94eb538196197d7e6c.png

Копируем код в Arduino IDE. Меняем параметры доступа к Wi-Fi, адрес сервера (указывается без https) и отпечаток SHA-1

#include 
#include  
#include 
#include 

#define LED 2 

const char *ssid = "ИМЯ_WiFi_сети"; 
const char *password = "ППАРОЛЬ_WiFi_сети";

const char *host = "test.username.repl.co";//адрес сервера без https://
const int httpsPort = 443;

//отпечаток SHA-1, который скопировали раньше
const char fingerprint[] PROGMEM = "AA BB CC DD EE FF 00 11 22 33 44 55 66";

void setup() {
  pinMode(LED, OUTPUT);
  digitalWrite(LED, HIGH);
  delay(1000);
  Serial.begin(115200);
  WiFi.mode(WIFI_OFF);
  delay(1000);
  WiFi.mode(WIFI_STA);
  
  WiFi.begin(ssid, password);//подключаемся к сети
  Serial.println("");

  Serial.print("Connecting");

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }


  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP()); 
}


void loop() {
  WiFiClientSecure httpsClient; 

  Serial.println(host);

  Serial.printf("Using fingerprint '%s'\n", fingerprint);
  httpsClient.setFingerprint(fingerprint);
  httpsClient.setTimeout(500); 
  delay(1000);
  //подключение к серверу
  Serial.print("HTTPS Connecting");
  int r=0; 
  while((!httpsClient.connect(host, httpsPort)) && (r < 30)){
      delay(100);
      Serial.print(".");
      r++;
  }
  if(r==30) {
    Serial.println("Connection failed");
  }
  else {
    Serial.println("Connected to web");
  }
  
  String ADCData, getData, Link; 
//создаем переменные, значения которых будут передаваться на сервер
  int temp = 15;//условная температура, которую в дальнейшем можно получать с датчика
  int light = !digitalRead(LED);//состояние встроенного светодиода, которы и есть СВЕТ в данном проекте
  Link = "/iot?temp="+String(temp)+"&light="+String(light);//собираем ссылку из параметров

  Serial.print("requesting URL: ");
  Serial.println(host+Link);
// выполняем переход по этой ссылке
  httpsClient.print(String("GET ") + Link + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +               
               "Connection: close\r\n\r\n");

  Serial.println("request sent");
                  
  while (httpsClient.connected()) {
    String line = httpsClient.readStringUntil('\n');
    if (line == "\r") {
      Serial.println("headers received");
      break;
    }
  }

  Serial.print("reply:");
//в переменную line записываем ответ сервера. В данном случае 0 или 1, команда от телеграм-бота
  
  String line;
  while(httpsClient.available()){        
    line = httpsClient.readStringUntil('\n');
    Serial.println(line);
    if (line=="0") {//включаем или выключаем свет
      digitalWrite(LED, HIGH);
    }
    if (line=="1") {
      digitalWrite(LED, LOW);
    }
  }

  Serial.println("closing connection");
    
  delay(500);//ждем 0.5с и повторяем
}

Загружаем код в ESP и наслаждаемся первым шагом к умному дома, сделанному своими руками.

Дальше полет фантазии в реализации не ограничен!

Такие дела! Успехов!

© Habrahabr.ru