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

от автора

Задача

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

Идея

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

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

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

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

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

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

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

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

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

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

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

После этого 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

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

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

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

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

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

#include <ESP8266WiFi.h> #include <WiFiClientSecure.h>  #include <ESP8266WebServer.h> #include <ESP8266HTTPClient.h>  #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 и наслаждаемся первым шагом к умному дому, сделанному своими руками.

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/710173/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *