Raspberry Pi Pico: Веб-сервер (MicroPython)

В этом руководстве вы узнаете, как создать базовый локальный веб-сервер на Raspberry Pi Pico для управления выходами и получения данных. В качестве примера мы создадим веб-страницу для включения и выключения светодиода, а также для получения случайных значений, генерируемых Pico. Этот пример можно легко модифицировать или расширить для управления несколькими выходами и получения данных с датчиков вместо случайных чисел.

Базовый веб-сервер Raspberry Pi Pico на MicroPython

Примечание

Это руководство совместимо только с Raspberry Pi Pico W, которая поставляется с поддержкой Wi-Fi. В этом руководстве, когда мы ссылаемся на Raspberry Pi Pico, мы имеем в виду Raspberry Pi Pico W.

Совет

Впервые работаете с Raspberry Pi Pico? Начните здесь: Начало работы с Raspberry Pi Pico.

В этом руководстве рассматриваются следующие темы:

Предварительные требования – прошивка MicroPython

Для выполнения этого руководства вам необходимо установить прошивку MicroPython на плату Raspberry Pi Pico. Вам также понадобится IDE для написания и загрузки кода на плату.

Рекомендуемая IDE для MicroPython на Raspberry Pi Pico – это Thonny IDE. Следуйте следующему руководству, чтобы узнать, как установить Thonny IDE, прошить прошивку MicroPython и загрузить код на плату.

Знакомство с базовыми концепциями веб-сервера

Совет

Уже знакомы с базовыми концепциями веб-сервера? Перейдите к разделу Обзор проекта.

Raspberry Pi Pico как веб-сервер

Простыми словами, веб-сервер – это «компьютер», который предоставляет веб-страницы. Он хранит файлы веб-сайта, включая все HTML-документы и связанные ресурсы, такие как изображения, CSS-стили, шрифты и/или другие файлы. Он также доставляет эти файлы на устройство веб-браузера пользователя, когда пользователь делает запрос к серверу.

Веб-сервер Raspberry Pi Pico

При использовании Raspberry Pi Pico в качестве веб-сервера вы, по сути, превращаете его в маленький компьютер, который хранит и предоставляет веб-страницы. Когда пользователи обращаются к веб-странице, их браузеры отправляют запросы к Pico, используя протокол передачи гипертекста (HTTP). Затем Pico отвечает, отправляя обратно запрошенную веб-страницу.

Клиент и сервер

Когда вы вводите URL в браузере, в фоновом режиме вы (клиент) отправляете запрос через протокол передачи гипертекста (HTTP) на сервер. Когда сервер получает запрос, он отправляет ответ через HTTP, и вы увидите запрошенную веб-страницу в своём браузере. Клиенты и серверы взаимодействуют через компьютерную сеть.

Простыми словами, клиенты делают запросы к серверам. Серверы обрабатывают запросы клиентов. В этом конкретном руководстве Raspberry Pi Pico будет сервером, а вы (ваш браузер) будете клиентом.

IP-адрес

IP-адрес – это числовая метка, присвоенная каждому устройству, подключённому к компьютерной сети. Это серия из четырёх значений, разделённых точками, где каждое значение находится в диапазоне от 0 до 255. Вот пример:

192.168.1.75

У себя дома ваши устройства подключены к частной сети через маршрутизатор (локальная сеть). Все устройства, подключённые к вашему маршрутизатору, являются частью вашей локальной сети. Внутри этой сети каждое устройство имеет свой собственный IP-адрес.

Устройства, подключённые к одному маршрутизатору, могут обращаться друг к другу по IP-адресу. Устройства за пределами вашей локальной сети не могут получить доступ к вашим локальным устройствам, используя их локальный IP-адрес.

Когда вы подключаете Raspberry Pi Pico к маршрутизатору, он становится частью вашей локальной сети. Таким образом, ему будет назначен уникальный локальный IP-адрес, и вы сможете получить к нему доступ со своего компьютера в локальной сети.

Сокеты

Мы создадим наш веб-сервер, используя сокеты и Python socket API.

Сокеты и socket API используются для отправки сообщений по сети. Они обеспечивают форму межпроцессного взаимодействия (IPC).

Сокеты можно использовать в клиент-серверных приложениях, где одна сторона выступает в роли сервера и ожидает подключений от клиентов – именно это мы здесь и сделаем (Raspberry Pi Pico является сервером, ожидающим подключений от клиентов).

Обзор проекта

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

Мы создадим простую веб-страницу с двумя кнопками для управления встроенным светодиодом Raspberry Pi Pico (включение и выключение). На веб-странице также есть ещё одна кнопка для запроса нового случайного значения (это можно легко изменить для запроса показаний датчиков).

Базовый пример веб-сервера Raspberry Pi Pico

Вот краткое объяснение того, как всё работает.

Когда вы вводите адрес Raspberry Pi Pico в веб-браузере, вы делаете запрос к Raspberry Pi Pico на корневой (/) URL. Когда он получает этот запрос, он отвечает HTML-кодом для построения веб-страницы, которую мы видели ранее.

Базовый веб-сервер Raspberry Pi Pico -- как работает запрос-ответ

Когда вы нажимаете кнопку Light on, выполняется запрос на URL /lighton?. Когда этот запрос получен, встроенный светодиод включается, и отправляется веб-страница с обновлённым состоянием светодиода.

Когда вы нажимаете кнопку Light off, выполняется запрос на URL /lightoff?. Когда это происходит, светодиод выключается, и отправляется веб-страница с текущим состоянием светодиода.

Базовый веб-сервер Raspberry Pi Pico -- диаграмма работы

Наконец, у нас также есть кнопка для запроса случайного значения от Raspberry Pi Pico. Это можно легко изменить для запроса показаний датчиков или любой другой полезной информации. Эта кнопка отправляет запрос на URL /value?. Когда Pico получает этот запрос, он генерирует новое случайное значение и отправляет HTML-страницу, обновлённую этим новым значением.

Базовый веб-сервер Raspberry Pi Pico -- запрос нового значения

HTML-страница

Ниже вы можете найти HTML-код, который мы будем использовать для построения веб-страницы:

<!DOCTYPE html>
<html>
<head>
    <title>Pico Web Server</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h1>Raspberry Pi Pico Web Server</h1>
    <h2>Led Control</h2>
    <form action="./lighton">
        <input type="submit" value="Light on" />
    </form>
    <br>
    <form action="./lightoff">
        <input type="submit" value="Light off" />
    </form>
    <p>LED state: {state}</p>
    <h2>Fetch New Value</h2>
    <form action="./value">
        <input type="submit" value="Fetch value" />
    </form>
    <p>Fetched value: {random_value}</p>
</body>
</html>

В этом конкретном примере мы сохраним содержимое HTML-файла в переменной в нашем скрипте MicroPython (но его также можно сохранить в файле в файловой системе Raspberry Pi Pico – это не рассматривается в данном руководстве).

Эта HTML-структура создаёт базовую веб-страницу для управления светодиодом и получения случайного значения для взаимодействия с веб-сервером Raspberry Pi Pico.

Переменные {state} и {random_value} предназначены для динамического заполнения значениями из серверной логики.

В итоге этот HTML создаёт три различные формы с кнопками.

Веб-сервер Raspberry Pi Pico – скрипт MicroPython

Создайте новый файл в Thonny IDE и скопируйте следующий код.

# Import necessary modules
import network
import socket
import time
import random
from machine import Pin

# Create an LED object on pin 'LED'
led = Pin('LED', Pin.OUT)

# Wi-Fi credentials
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'

# HTML template for the webpage
def webpage(random_value, state):
    html = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>Pico Web Server</title>
            <meta name="viewport" content="width=device-width, initial-scale=1">
        </head>
        <body>
            <h1>Raspberry Pi Pico Web Server</h1>
            <h2>Led Control</h2>
            <form action="./lighton">
                <input type="submit" value="Light on" />
            </form>
            <br>
            <form action="./lightoff">
                <input type="submit" value="Light off" />
            </form>
            <p>LED state: {state}</p>
            <h2>Fetch New Value</h2>
            <form action="./value">
                <input type="submit" value="Fetch value" />
            </form>
            <p>Fetched value: {random_value}</p>
        </body>
        </html>
        """
    return str(html)

# Connect to WLAN
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

# Wait for Wi-Fi connection
connection_timeout = 10
while connection_timeout > 0:
    if wlan.status() >= 3:
        break
    connection_timeout -= 1
    print('Waiting for Wi-Fi connection...')
    time.sleep(1)

# Check if connection is successful
if wlan.status() != 3:
    raise RuntimeError('Failed to establish a network connection')
else:
    print('Connection successful!')
    network_info = wlan.ifconfig()
    print('IP address:', network_info[0])

# Set up socket and start listening
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen()

print('Listening on', addr)

# Initialize variables
state = "OFF"
random_value = 0

# Main loop to listen for connections
while True:
    try:
        conn, addr = s.accept()
        print('Got a connection from', addr)

        # Receive and parse the request
        request = conn.recv(1024)
        request = str(request)
        print('Request content = %s' % request)

        try:
            request = request.split()[1]
            print('Request:', request)
        except IndexError:
            pass

        # Process the request and update variables
        if request == '/lighton?':
            print("LED on")
            led.value(1)
            state = "ON"
        elif request == '/lightoff?':
            led.value(0)
            state = 'OFF'
        elif request == '/value?':
            random_value = random.randint(0, 20)

        # Generate HTML response
        response = webpage(random_value, state)

        # Send the HTTP response and close the connection
        conn.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        conn.send(response)
        conn.close()

    except OSError as e:
        conn.close()
        print('Connection closed')

Вам нужно просто вставить свой SSID и пароль в следующие переменные, и код должен заработать сразу.

# Wi-Fi credentials
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'

Как работает код?

Продолжайте чтение, чтобы узнать, как работает код, или перейдите к разделу Демонстрация веб-сервера Raspberry Pi Pico.

Импорт библиотек

Начните с импорта необходимых модулей, включая network для Wi-Fi, socket для связи между клиентом и сервером, time для задержек, random для генерации случайных чисел и Pin из machine для управления GPIO-выводами.

import network
import socket
import time
import random
from machine import Pin

Объявление вывода светодиода

Создайте объект Pin с именем led, который соответствует встроенному светодиоду Raspberry Pi Pico. Он настроен как выходной вывод. Он интерпретирует 'LED' как встроенный светодиод.

led = Pin('LED', Pin.OUT)

Установите учётные данные Wi-Fi для сети, к которой вы хотите подключиться. Измените эти переменные, указав свои собственные учётные данные.

# Wi-Fi credentials
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'

Создание веб-страницы

Для генерации веб-страницы мы создали функцию webpage(), которая принимает в качестве аргументов случайное значение и состояние светодиода. Мы генерируем веб-страницу в функции, потому что это позволяет нам динамически изменять её содержимое в момент отправки клиенту. Таким образом, мы можем отправить страницу с самой последней информацией о состоянии GPIO или случайном значении.

# HTML template for the webpage
def webpage(random_value, state):
    html = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>Pico Web Server</title>
            <meta name="viewport" content="width=device-width, initial-scale=1">
        </head>
        <body>
            <h1>Raspberry Pi Pico Web Server</h1>
            <h2>Led Control</h2>
            <form action="./lighton">
                <input type="submit" value="Light on" />
            </form>
            <br>
            <form action="./lightoff">
                <input type="submit" value="Light off" />
            </form>
            <p>LED state: {state}</p>
            <h2>Fetch New Value</h2>
            <form action="./value">
                <input type="submit" value="Fetch value" />
            </form>
            <p>Fetched value: {random_value}</p>
        </body>
        </html>
        """
    return str(html)

Чтобы лучше понять, как это работает, и использование f-строк вместе с фигурными скобками {} для включения переменных в строку HTML, продолжайте чтение.

Тройные кавычки («»») используются для создания многострочной строки. Это позволяет легко форматировать HTML-содержимое без необходимости конкатенации нескольких строк. Наше HTML-содержимое более читаемо таким образом, но вы можете объединить всё в одну строку, если хотите.

Буква f перед открывающей тройной кавычкой обозначает f-строку, которая является способом встраивания выражений внутри строковых литералов. В данном случае это позволяет включать переменные (random_value и state) непосредственно в строку.

html = f"""

Фигурные скобки {} внутри f-строки выступают в качестве заполнителей для переменных. Переменные внутри фигурных скобок заменяются их фактическими значениями при форматировании строки.

В итоге {state} и {random_value} в HTML-содержимом являются заполнителями для фактических значений переменных state и random_value.

<p>LED state: {state}</p>
<p>Fetched value: {random_value}</p>

Это динамическое форматирование строк упрощает встраивание значений переменных непосредственно в HTML-содержимое, обеспечивая чистый и читаемый способ генерации динамических веб-страниц.

Когда мы вызываем эту функцию, она возвращает HTML-страницу с текущим случайным значением и состоянием светодиода.

Подключение к Интернету

Следующие строки отвечают за подключение вашего Raspberry Pi Pico к локальной сети с использованием SSID и пароля, которые вы определили ранее.

# Connect to WLAN
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

Дождитесь установки Wi-Fi-соединения. Проверяется статус подключения, и если wlan.status() >= 3 (это означает, что он либо подключился к сети, либо не смог подключиться), цикл завершается. Ожидание длится максимум 10 секунд.

Метод wlan.status, как следует из названия, проверяет статус Wi-Fi-соединения Raspberry Pi. Этот метод возвращает целое число со следующим значением:

  • 0: WLAN не включён

  • 1: WLAN в данный момент сканирует сети

  • 2: WLAN подключается к сети

  • 3: WLAN подключён к сети

  • 4: WLAN не удалось подключиться к сети

Затем мы проверяем, подключён ли он или нет (мы знаем, что статус = 3 означает подключение). Если wlan.status() отличается от 3, мы знаем, что не удалось установить соединение.

В противном случае это означает, что мы успешно подключились. В случае успеха мы можем получить информацию о сети с помощью метода ifconfig(). Этот метод возвращает кортеж со следующей информацией:

network_info[0]: IP-адрес, назначенный Pico в сети.
network_info[1]: Маска подсети.
network_info[2]: IP-адрес шлюза.
network_info[3]: IP-адрес DNS-сервера (Domain Name System).

Нам нужен только IP-адрес, поэтому мы берём первый элемент массива (network_info[0]).

else:
    print('Connection successful!')
    network_info = wlan.ifconfig()
    print('IP address:', network_info[0])

Создание сокет-сервера

После установки соединения с нашей сетью нам нужно создать прослушивающий сокет для приёма входящих запросов и отправки HTML-текста в ответ. Для лучшего понимания на следующем рисунке показана диаграмма создания сокетов для клиент-серверного взаимодействия:

Диаграмма создания сокет-сервера

Настройте сокет для прослушивания на порту 80. Используется адрес 0.0.0.0, что означает, что он будет прослушивать все доступные сетевые интерфейсы. Опция SO_REUSEADDR позволяет сокету повторно использовать адрес (таким образом у вас не будет проблем с уже используемыми адресами при попытке запустить веб-сервер после первого раза).

addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen()

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

addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]

Создайте сокет с помощью socket.socket().

s = socket.socket()

Далее привяжите сокет к адресу (сетевому интерфейсу и номеру порта) с помощью метода bind(). Метод bind() принимает кортеж с IP-адресом и номером порта, который мы ранее сохранили в переменной addr:

s.bind(addr)

Переведите сокет в режим прослушивания. Это означает, что теперь он готов принимать входящие подключения.

s.listen()

Инициализация переменных

Инициализируйте переменные для состояния светодиода (state) и для случайного значения (random_value).

# Initialize variables
state = "OFF"
random_value = 0

Прослушивание подключений

В цикле while мы прослушиваем запросы и отправляем ответы. Когда клиент подключается, сервер вызывает метод accept() для принятия подключения. При подключении клиента он сохраняет новый объект сокета для приёма и отправки данных в переменную conn, а адрес клиента для подключения к серверу – в переменную addr.

conn, addr = s.accept()

Затем выведите адрес клиента, сохранённый в переменной addr.

print('Got a connection from', addr)

Обмен данными между клиентом и сервером осуществляется с помощью методов send() и recv().

Следующая строка получает запрос, полученный на вновь созданном сокете, и сохраняет его в переменной request.

request = conn.recv(1024)

Метод recv() получает данные от клиентского сокета (помните, что мы создали новый объект сокета в переменной conn). Аргумент метода recv() указывает максимальный объём данных, который может быть получен за один раз, в байтах.

Следующая строка просто выводит содержимое запроса:

print('Request content = %s' % request)

Получение содержимого запроса

Затем мы получаем содержимое запроса с помощью request.split()[1]. Это разбивает полученную строку HTTP-запроса на список слов и берёт второй элемент (индекс 1) списка. Обычно это путь HTTP-запроса, который является информацией, необходимой нам для определения того, какая кнопка была нажата на веб-интерфейсе.

try:
    request = request.split()[1]
    print('Request:', request)
except IndexError:
    pass

Обработка запросов

Затем мы проверяем содержимое запроса и действуем соответственно. Если запрос поступил по пути /lighton?, мы включаем светодиод и обновляем переменную state до 'ON'.

if request == '/lighton?':
    print("LED on")
    led.value(1)
    state = "ON"

Если запрос /lightoff?, мы выключаем светодиод и обновляем переменную state до 'OFF'.

elif request == '/lightoff?':
    led.value(0)
    state = 'OFF'

Наконец, если запрос поступил по пути /value?, мы генерируем новое случайное значение и сохраняем его в переменной random_value.

elif request == '/value?':
    random_value = random.randint(0, 20)

Как мы упоминали ранее, вместо генерации случайного значения вы можете получать показания датчиков.

Отправка ответа

После обработки запроса мы вызовем функцию webpage() с аргументами random_value и state для генерации HTML-страницы с обновлёнными значениями. Содержимое для генерации веб-страницы сохраняется в переменной response.

response = webpage(random_value, state)

Наконец, отправьте ответ клиенту сокета с помощью метода send():

conn.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
conn.send(response)

В конце закройте созданный сокет.

conn.close()

Демонстрация веб-сервера Raspberry Pi Pico

Запустите предыдущий код на вашем Raspberry Pi Pico. Вам нужно просто нажать зелёную кнопку запуска в Thonny IDE.

Кнопка запуска в Thonny IDE

IP-адрес Raspberry Pi будет напечатан через несколько секунд. В моём случае это 192.168.1.82.

Получение IP-адреса Raspberry Pi Pico в Thonny IDE

Откройте веб-браузер в вашей локальной сети и введите IP-адрес вашего Raspberry Pi Pico. Должна загрузиться следующая веб-страница.

Базовый веб-сервер Raspberry Pi Pico в веб-браузере

Вы можете нажимать кнопки Light on и Light off для включения и выключения встроенного светодиода Raspberry Pi Pico.

Встроенный светодиод Raspberry Pi Pico W выключен Встроенный светодиод Raspberry Pi Pico W включён

Вы также можете нажать кнопку Fetch value, чтобы запросить новое случайно сгенерированное значение от Raspberry Pi Pico. Он должен ответить веб-страницей, содержащей самое новое значение.

Веб-сервер Raspberry Pi Pico -- запрос нового значения

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

Веб-сервер также доступен со смартфона, если он подключён к той же сети.

Веб-сервер Raspberry Pi Pico на смартфоне

Если вы хотите, чтобы Raspberry Pi Pico запускал веб-сервер без подключения к компьютеру, вам нужно загрузить код как main.py в файловую систему Raspberry Pi Pico. Для этого, после копирования кода в новый файл, перейдите в File > Save as и выберите Raspberry Pi Pico.

Сохранение файлов в Raspberry Pi Pico через Thonny IDE

Назовите файл main.py и нажмите OK, чтобы сохранить файл на Raspberry Pi Pico. Теперь он будет запускать файл main.py при загрузке без необходимости быть подключённым к компьютеру.

Сохранение файла main.py в MicroPython через Thonny IDE

Заключение

В этом руководстве вы изучили основы создания веб-сервера с помощью Raspberry Pi Pico. В качестве примера мы создали простую страницу с кнопками для управления GPIO и разделом для запроса нового случайного значения от платы.

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

Надеемся, что это руководство было для вас полезным.