Реклама ⓘ
Главная » Arduino
Призовой фонд
на июнь 2024 г.

Реклама ⓘ

IoT устройство на ESP32

Введение

Написание графического интерфейса для взаимодействия с микроконтроллером может занять достаточно много времени на изучение фреймворков, а также написание протокола для взаимодействия через UART. При прототипировании устройств это может занять много времени. Взаимодействие через браузер позволяет работать как с телефона, так и компьютера, при этом взаимодействие будет происходит через i2c или SPI, что освобождает от необходимости использования протоколов. Платформа ESP32 c прошивкой micropython позволяет не только работать в качестве web-server, но и вести лог-файл событий с записью прямо во флеш-память, откуда потом файл можно скопировать.

Установка прошивки на ESP32

После подключения ESP32 необходимо установить драйверы на ESP32 чаще всего ставят CH340 или CP2102. После их установки на потребуется установить программу, а далее прошить в нее интерпретатор MicroPython, в котором мы будем писать наш сервер.

После установки прошивки нам необходимо создать новый файл main.py и сохранить его на устройстве. Одна из причин, написание я программы в main.py, а не в boot.py, это более стабильная работа сигнала прерывания выполнения бесконечного цикла, которая вызывается комбинацией клавиш Ctrl+C. Если вы используете прерывание по таймеру, то выполнить прерывание работы порой можно только при перезагрузки с помощью кнопки EN или комбинацией клавиш Ctrl+D с последующим вызовом комбинаций Cntr+C, для удобства можно ставить задержку перед запуском основного скрипта на 2 секунды time.sleep(2)

C помощью боковой панели Файлы мы можем копировать в память файлы HTML, CSS, javascript и фотографии.

Пришло время написать первую прошивку для тестирования. Скрипт мы будет писать в main.py. В качестве примера помигает светодиодом, а также выведем в консоль время включения-отключения и количество доступной памяти. Консоль - это наш главный помощник в отладке кода на micropython. Для прерывания программы нажмем Ctrl+C.

from machine import Pin,freq
import time
import gc
import os
import machine
 
if __name__ == "__main__":
  # инициализируем пин для управления светодиодом
   led=Pin(2,Pin.OUT)
   m=os.statvfs("/")
   f=machine.freq()   
   while True:
       led.value(1)
       data=time.localtime()
       print(f"{data[3]:02d}:{data[4]:02d}:{data[5]:02d} - led ON")
       mem=gc.mem_free()
       print(f"Осталось памяти ОЗУ {mem} байт")
       time.sleep_ms(100)
       led.value(0)
       data=time.localtime()
       print(f"{data[3]:02d}:{data[4]:02d}:{data[5]:02d} - led OFF")
       mem=gc.mem_free()
       print(f"Осталось памяти ОЗУ {mem} байт")
       print(f"Программной памяти доступно {m[0]*m[3]} байт")
       print(f"частоты работы {f} Гц")
       time.sleep_ms(100)   

 

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

Статический веб-сервер

Пришло время реализовать статическую веб-страницу. На большинстве ресурсов код HTML страницы хранился прямо в коде сервера. Мы реализуем подход, когда наш сервер будет загружать HTML-код из файла, а также подгружать картинки по запросу браузера. Данный подход удобнее, так как позволяет в начале протестировать нашу HTML страницу в браузере, а затем просто ее загрузить в память ESP32. Для управления светодиодом мы будем использовать не ссылку на мнимую страницу, а создавать POST-запрос. Динамичность нашей страничке будет придавать наш веб-сервер, который будет заменять шаблонные выражения на реальные данные. В качестве шаблонного выражения в index.html используется #url_bulb#, который в зависимости от состояния светодиода меняется на название файл bulb_on.png или bulb_off.png

Итоговый вид нашей странички будет такой

 

Ниже представлен вид, который мы будем видеть на стороне нашего сервера.

Текст index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> Умная лампочка </title>

<style>
.btn{
min-width: 100px;
font-size: 12pt;
}
</style>

</head>
<h3>Управление лампочкой</h3>
<img src="#url_bulb#">

<br>
<form action=/ method="post">
<button class="btn" type="submit" name="switch" value="1">Включить</button>
</form>

<br>
<form action=/ method="post">
<button class="btn" type="submit" name="switch" value="0">Отключить</button>
</form>

</html>

А теперь текст нашего сервера который храниться в файле main.py

from machine import Pin,reset
import time
import network
import socket
import io
import gc

if __name__ == "__main__":
    # инициализируем пин для управления светодиодом 
    led=Pin(2,Pin.OUT)
    led_status=0
    led.value(led_status)
    
    #Настройка точки доступа
    ap_if = network.WLAN(network.AP_IF)
    ap_if.active(True)
    ap_if.config(essid="MyPoint", password="12345678")
    #Настройка проверки пароля точки доступа
    #ap_if.config(authmode=network.AUTH_WPA_WPA2_PSK)
    # Создание сервера
    # ожидаем создание сети
    while ap_if.active() == False:
      pass
    print('Connection successful')
    print(ap_if.ifconfig())
    # создаем сокет для прием сообщений
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.bind(('', 80))
    except OSError:
        reset()
    s.listen(5)
    while True:
        # ожидаем входящих сообщений
        conn, addr = s.accept()
        data=time.localtime()
        print(f"Время {data[3]:02d}:{data[4]:02d}:{data[5]:02d}")
        mem=gc.mem_free()
        print(f"Осталось памяти начало {mem}")
        #print(f"Got a connection from {str(adr)}")
        request = conn.recv(1024)
        # переводим из бинарного формата в строку
        request=str(request)
        # печатаем входящий запрос
        print(request)
        conn.send('HTTP/1.1 200 OK\n')
        conn.send('Content-Type: text/html\n')
        conn.send('Connection: close\n\n')
        # запрос на загрузка картинки
        if request.find("bulb_off")>0:
            with io.open("bulb_off.png","rb") as file:
                st=file.read()
            conn.sendall(st)
            conn.close()
            del st
            print(f"Осталось памяти в конце {mem}")
            continue
        elif request.find("bulb_on")>0:
            with io.open("bulb_on.png","rb") as file:
                st=file.read()
            conn.sendall(st)
            conn.close()
            del st
            print(f"Осталось памяти в конце {mem}")
            continue
        # обработка событий кнопок и загрузки основной страницы
        if request.find("switch=1")>0:
            led_status=1
        elif request.find("switch=0")>0:
            led_status=0
        led.value(led_status)
        with io.open("index.html","r") as file:
            st=file.read()
        if led_status==1:
            st=st.replace("#url_bulb#","bulb_on.png")
        else:
            st=st.replace("#url_bulb#","bulb_off.png")
        conn.sendall(st) 
        conn.close()
        del st
        mem=gc.mem_free()
        print(f"Осталось памяти в конце {mem}")

Сервер настроен на создание точки доступа, к которой подключается клиент. Если требуется настроить защиту паролем, то необходимо раскомментировать строку ap_if.config(authmode=network.AUTH_WPA_WPA2_PSK)

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

При запуске в консоли будет текст запросов со стороны веб-браузера

Рассмотрим сообщения веб-браузера серверу.

При запросе страницы по адресу 192.168.4.1 придет следующее сообщение

Время 21:49:31
Осталось памяти начало 145968
b'GET / HTTP/1.1\r\nHost: 192.168.4.1\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: ru-RU,ru;q=0.9,en;q=0.8\r\n\r\n'
Осталось памяти в конце 143360

В ответ на это сервер отправить файл index.html

Загрузив файл index.html, браузер запросит фотографию лампочки, шаблон которой был заменен на название актуальной фотографии.

Время 21:49:31
Осталось памяти начало 143056
b'GET /bulb_off.png HTTP/1.1\r\nHost: 192.168.4.1\r\nConnection: keep-alive\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\nAccept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\nReferer: http://192.168.4.1/\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: ru-RU,ru;q=0.9,en;q=0.8\r\n\r\n'
Осталось памяти в конце 143056

Сервер находит название файла bulb_off, и открывает в бинарном виде файл bulb_off.png, после чего отправляет набор байтов веб-браузеру. Существует альтернативный способ отправить файл фотографии, через формат base64. Для этого фото в начале нужно конвертировать на сайте https://www.base64-image.de, а затем в атрибуте src вместо адреса картинки ввести код картинки сгенерированный сайтом. 

<img src="data:image/jpeg;base64,/9j/4AAQSkZ...JRgABAQEASABIAAD//gA9Q1">

Код картинки получается достаточно длинным и дополнительно единовременно грузит нашу кучу. Для картинки размером в 10 кб размер строки у меня получился более 10 тысяч символов. Я не рекомендую использовать данный способ для крупных изображений, если только для очень маленьких картинок-иконок.

Продолжим рассматривать запросы браузера серверу

При нажатии на кнопку браузер будет посылать нам POST запрос на включение или отключение лампочки

Время 21:49:33
Осталось памяти начало 135536
b'POST / HTTP/1.1\r\nHost: 192.168.4.1\r\nConnection: keep-alive\r\nContent-Length: 8\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nOrigin: http://192.168.4.1\r\nContent-Type: application/x-www-form-urlencoded\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\nReferer: http://192.168.4.1/\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: ru-RU,ru;q=0.9,en;q=0.8\r\n\r\nswitch=1'
Осталось памяти в конце 128544

А ниже запрос на выключение

 Время 21:49:47
Осталось памяти начало 118816
b'POST / HTTP/1.1\r\nHost: 192.168.4.1\r\nConnection: keep-alive\r\nContent-Length: 8\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nOrigin: http://192.168.4.1\r\nContent-Type: application/x-www-form-urlencoded\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\nReferer: http://192.168.4.1/\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: ru-RU,ru;q=0.9,en;q=0.8\r\n\r\nswitch=0'
Осталось памяти в конце 111824

При этом если бы у нас были в форме еще <input> теги, то их атрибуты name и value отправились бы в одном запросе через амперсанд, что очень удобно. Следующий пример покажет отправку формы со всеми полями

Отправка нескольких данных из формы серверу

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

Мигание будет происходит по прерыванию таймера. Прерывание таймера - это один из способов осуществить опрос кнопок, датчиков, отправку сообщений. 

На HTML-странице все параметры которые должны быть заменены на реальные данные обрамлены решеткой #параметр#. Для оформления страницы были внедрены стили CSS, которые при небольшой доработки можно выделить в отдельный файл, аналогично файлу HTML. 

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> Умная лампочка </title>

<style>
.btn{
min-width: 100px;
font-size: 12pt;
}

.lbl{
font-size: 14pt;
}

.switch_on
{
border: 5px inset; 
width: 250px;
padding: 5px 15px;
/*text-align: center;*/
}

</style>

</head>
<h3>Управление лампочкой</h3>
<img src="#bulb_png#">

<br>

<form class="switch_on" action=/ method="post">
	<p>Настройка работы</p>
	<p>
		<input type="radio" name="mode" value="on" #checked_on#> освещение
	</p>
	<p>
		<input type="radio" name="mode" value="blink" #checked_blink#> мигание
	</p>
	<p>
		Период мс:<input type="number" class="lbl" name="period" value="#period_value#" min="100" max="2000">
	</p>
	
	<p>
		<button class="btn" type="submit">Включить</button>
	</p>
</form>

<br>
<form class="switch_on" action=/ method="post">
<button class="btn" type="submit" name="mode" value="off">Отключить</button>
</form>


</html>

А текст нашего сервера стал более сложный. В нем добавился блок автоматической отправки фотографий, а параметры стали приниматься в словарь, который полностью соответствует структуре JSON.

from machine import Pin,reset,Timer
import time
import network
import socket
import io
import gc

# храним все настройки в словаре
# для javascript формат словаря называется JSON
param={
    "mode":"off",      # режим работы
    "mode_last":"off", # хранит предыдущий результат
    "period":"1000"    # период мигания
    
    }
# обработчик прерываний
def ledBlink():
    global param
    #global led
    #print("прерывание")
    if param["mode"]=="blink":
        if led.value()==1:
            led.value(0)
        else:
            led.value(1)
    
if __name__ == "__main__":
    time.sleep(2)
    # инициализируем пин для управления светодиодом
    led=Pin(2,Pin.OUT)
    if param["mode"]=="on":
        led.value(1)
    else:
        led.value(0)
    # инициализация таймера
    tim1 = Timer(1)
    tim1.init(period=int(param["period"]), mode=Timer.PERIODIC, callback=lambda t:ledBlink() )
    #Настройка точки доступа
    ap_if = network.WLAN(network.AP_IF)
    ap_if.active(True)
    ap_if.config(essid="MyPoint", password="12345678")
    #Настройка проверки пароля точки доступа
    #ap_if.config(authmode=network.AUTH_WPA_WPA2_PSK)
    # Создание сервера
    # ожидаем создание сети
    while ap_if.active() == False:
      pass
    print('Connection successful')
    print(ap_if.ifconfig())
    # создаем сокет для прием сообщений
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.bind(('', 80))
    except OSError:
        reset()
    s.listen(5)
    while True:
        # ожидаем входящих сообщений
        conn, addr = s.accept()
        data=time.localtime()
        print(f"Время {data[3]:02d}:{data[4]:02d}:{data[5]:02d}")
        mem=gc.mem_free()
        print(f"Осталось памяти начало {mem}")
        #print(f"Got a connection from {str(adr)}")
        request = conn.recv(1024)
        # переводим из бинарного формата в строку
        request=str(request)
        # печатаем входящий запрос
        print(request)
        conn.send('HTTP/1.1 200 OK\n')
        conn.send('Content-Type: text/html\n')
        conn.send('Connection: close\n\n')
        #-------------------------------------------------------
        # запрос на загрузка любой картинки по запросу
        if request.find(".png")>0:
            # делим строку на слова и оставляем только 2 элемент
            # который является именем
            name_img=request.split()[1]
            # удаляем первый символ
            name_img=name_img[1:]
            try:
                with io.open(name_img,"rb") as file:
                    st=file.read()
                conn.sendall(st)
                conn.close()
                del st
                print(f"Файл {name_img} отправлен");
                print(f"Осталось памяти в конце {mem}")
                continue
            except OSError:
                print(f"Файл {name_img} не найден");
        #-------------------------------------------------------
        # обработка событий кнопок и загрузки основной страницы
        if request.find("POST")>0:
            # делим на слова и берем последнее
            prm=request.split("\\n")[-1]
            print(prm)
            # удаляем последний апостроф
            prm=prm[:-1]
            print(prm)
            # делим на параметры
            prm=prm.split("&")
            for i in prm:
                temp=i.split("=")
                param[temp[0]]=temp[1]
            print(param)
        #----------Пункт 1--------------------------------------
        # открываем заготовку html страницы 
        with io.open("index.html","r") as file:
            st=file.read()
        #----------Пункт 2------------------------------------
        # заменяем шаблоны на актуальные названия файлов
        if param["mode"]=="on":
            st=st.replace("#bulb_png#","bulb_on.png")
            param["mode_last"]="on"
            print(f"режим вкл {param}")
        elif param["mode"]=="blink":
            st=st.replace("#bulb_png#","bulb_blink.png")
            param["mode_last"]="blink"
            print(f"режим мигания {param}")
        else: # Режим выключен
            st=st.replace("#bulb_png#","bulb_off.png")
            print(f"режим выкл {param}")
        # ставим указатель
        if param["mode_last"]=="blink":
            st=st.replace("#checked_on#","")
            st=st.replace("#checked_blink#","checked")
        else:    
            st=st.replace("#checked_on#","checked")
            st=st.replace("#checked_blink#","")
        
            
        # заменяем значение частоты на последнее текущее
        st=st.replace("#period_value#",param["period"])    
        #----------Пункт 3-----------------------------------
        # Отправляем данные браузеру
        conn.sendall(st) 
        conn.close()
        del st
        mem=gc.mem_free()
        print(f"Осталось памяти в конце {mem}")
        #----------Пункт 4----------------------------------
        # настраиваем режим работы нашего светодиода
        if param["mode"]=="on":
            led.value(1)
            tim1.init(period=1000, mode=Timer.PERIODIC, callback=lambda t:ledBlink() )
        elif param["mode"]=="blink":
            tim1.init(period=int(param["period"]), mode=Timer.PERIODIC, callback=lambda t:ledBlink() )
        else:
            led.value(0)
            tim1.init(period=1000, mode=Timer.PERIODIC, callback=lambda t:ledBlink() )
        

Надеюсь, мой пример будет полезным людям, кто хочет начать разрабатывать IoT. 

Из минусов данного подхода хочется отметить, что страничка тормозит, данные обновляется только при обновлении страницы целиком. Кнопки иногда срабатывают не с первого раза, это связанно с частым вызовом сборщика мусора. Автоматическое обновление можно настроить вставив в раздел <head> тег <meta http-equiv="refresh" content="15">. Однако проблему актуальных данных это не решает. При увеличении количества шаблонов, в которых необходимо делать замены, будет увеличиваться потребление памяти на операцию замены данных. Вручную менять шаблоны достаточно неудобно.

Для решение этих проблем необходимо, чтобы ESP32 отсылал только данных, а браузер с помощью javascript сам редактировал страницу. Данная технология называется AJAX. Реализацию данного подхода можно будет посмотреть в следующей статье.

Прикладываю архив с листингам для удобства читателей.

Прикрепленные файлы:

Теги:

Опубликована: 0 0
Я собрал 0 0
x

Оценить статью

  • Техническая грамотность
  • Актуальность материала
  • Изложение материала
  • Полезность устройства
  • Повторяемость устройства
  • Орфография
0

Средний балл статьи: 0 Проголосовало: 0 чел.

Комментарии (5) | Я собрал (0) | Подписаться

0
Публикатор #
На форуме автоматически создана тема для обсуждения статьи.
Ответить
0
DerSkythe #
Тормоза, лаги, неправильные отступы.... Это Может, всё-таки на C/C++. Ну или хотя бы на Toit/ или dotnetNanoframework?
Ответить
0

[Автор]
Aleksey1408 #
В этом фреймворке не реализованна библиотека для сокетов
Ответить
0
DerSkythe #
Без сокетов они бы не могли в TCP/IP. Что вы подразумеваете под библиотекой сокетов?

Во всех предложенных есть, помимо сокетов, даже большая абстракция: веб-сервер, настраиваемый в пару строчек.
Вот примеры:
https://github.com/nanoframework/nanoFramework.WebServer
https://docs.toit.io/tutorials/network/http-file-server
Ответить
0
Игорь #
Хочу обратить внимание на бэйсик для ESP8266, ESP32 (C3, S2, S3). На мой взгляд, на нем можно удобно и быстро создавать интересные прототипы.
P.S. Русский язык в тексте программ и html только в utf-8, но строковые функции многобайтные символы не учитывают.
Ответить
Добавить комментарий
Имя:
E-mail:
не публикуется
Текст:
Защита от спама:
В чем измеряется электрическая мощность?
Файлы:
 
Для выбора нескольких файлов использйте CTRL

Модуль измерения тока на ACS712 (30А)
Модуль измерения тока на ACS712 (30А)
Катушка Тесла Макетная плата для пайки (10 шт)
вверх