Управление микроконтроллером через telegram-бот с обратной связью
Задача
Нужно управлять различными устройствами: свет, вентиляция, полив, а также получать нужные данные от микроконтроллера.
При этом для учебно-тренировочных или DIY-задач совершенно не хочется задействовать дополнительные устройства, на которых будет размещаться сервер и уж тем более не оплачивать внешний статический ip-адрес.
Идея
Обеспечить выход в интернет с микроконтроллера, запустить два скрипта: веб-сервер для приема информации от микроконтроллера и телеграм-бот для связи с пользователем.
Веб-сервер на Flask и бота будем размещать на ReplIt. Как это сделать бесплатно с работой 24/7 описано в статье Как хостить телеграм-бота
Первая попытка была использовать Arduino + Ethernet-модуль W5500, эта связка заработала только внутри локальной сети и провалилась при переносе на ReplIt, т.к. ссылка веб-сервера оказалась доступна только по протоколу https, который Arduino не поддерживает.
Решение нашлось в виде платы 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
Далее открываем в левом меню Board 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
#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 и наслаждаемся первым шагом к умному дома, сделанному своими руками.
Дальше полет фантазии в реализации не ограничен!
Такие дела! Успехов!