Raspberry Pi Pico W: асинхронный веб-сервер (MicroPython)

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

Raspberry Pi Pico W асинхронный веб-сервер MicroPython

Мы создадим веб-страницу для включения и выключения светодиода, а также для получения случайных значений, генерируемых 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 для написания и загрузки кода на плату.

Прошивка MicroPython для платы Raspberry Pi Pico

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

Базовые концепции веб-сервера

Если вы не знакомы с базовыми концепциями веб-сервера, мы рекомендуем сначала ознакомиться с этим руководством:

Базовые концепции веб-сервера для платы Raspberry Pi Pico W

Обзор проекта асинхронного веб-сервера Raspberry Pi Pico

Давайте кратко рассмотрим проект, который мы будем создавать, чтобы было проще понять код в дальнейшем.

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

Асинхронный веб-сервер Raspberry Pi Pico

Вот что делает наш пример:

  • Создает веб-сервер, который обслуживает веб-страницу с:

    • двумя кнопками для включения и выключения светодиода (GPIO 19)

    • разделом для отображения случайного значения (можно заменить показаниями датчика)

  • Запускает параллельную задачу мигания светодиода (GPIO 20)

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

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

Модуль asyncio MicroPython

Для создания асинхронного веб-сервера мы воспользуемся модулем asyncio MicroPython. Этот модуль уже включен в прошивку MicroPython по умолчанию, поэтому вам не нужно устанавливать дополнительные модули.

Преимущества асинхронного программирования

Модуль asyncio MicroPython позволяет запускать несколько задач параллельно, создавая иллюзию многозадачности и избегая блокировки кода на длительных операциях.

Синхронное и асинхронное программирование

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

Модуль asyncio MicroPython — это легковесный фреймворк асинхронного ввода-вывода, вдохновленный модулем asyncio Python. Вы можете ознакомиться со всеми подробностями об этом модуле MicroPython по следующей ссылке: документация модуля asyncio MicroPython.

Сборка схемы

Для тестирования этого проекта подключите два светодиода к Raspberry Pi Pico. Один светодиод подключен к GPIO 19, а другой — к GPIO 20. Вы можете использовать схему ниже в качестве справки.

Необходимые компоненты

Raspberry Pi Pico W

2x светодиода

2x резистора 220 Ом

Макетная плата

Соединительные провода

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

Raspberry Pi Pico W схема подключения двух светодиодов

Асинхронный веб-сервер Raspberry Pi Pico – код MicroPython

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

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

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

# Create several LEDs
led_blink = Pin(20, Pin.OUT)
led_control = Pin(19, Pin.OUT)

# Initialize variables
state = "OFF"
random_value = 0

# 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)

# Init Wi-Fi Interface
def init_wifi(ssid, password):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    # Connect to your network
    wlan.connect(ssid, password)
    # Wait for Wi-Fi connection
    connection_timeout = 10
    while connection_timeout > 0:
        print(wlan.status())
        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:
        print('Failed to connect to Wi-Fi')
        return False
    else:
        print('Connection successful!')
        network_info = wlan.ifconfig()
        print('IP address:', network_info[0])
        return True

# Asynchronous functio to handle client's requests
async def handle_client(reader, writer):
    global state

    print("Client connected")
    request_line = await reader.readline()
    print('Request:', request_line)

    # Skip HTTP request headers
    while await reader.readline() != b"\r\n":
        pass

    request = str(request_line, 'utf-8').split()[1]
    print('Request:', request)

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

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

    # Send the HTTP response and close the connection
    writer.write('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
    writer.write(response)
    await writer.drain()
    await writer.wait_closed()
    print('Client Disconnected')

async def blink_led():
    while True:
        led_blink.toggle()  # Toggle LED state
        await asyncio.sleep(0.5)  # Blink interval

async def main():
    if not init_wifi(ssid, password):
        print('Exiting program.')
        return

    # Start the server and run the event loop
    print('Setting up server')
    server = asyncio.start_server(handle_client, "0.0.0.0", 80)
    asyncio.create_task(server)
    asyncio.create_task(blink_led())

    while True:
        # Add other tasks that you might need to do in the loop
        await asyncio.sleep(5)
        print('This message will be printed every 5 seconds')


# Create an Event Loop
loop = asyncio.get_event_loop()
# Create a task to run the main function
loop.create_task(main())

try:
    # Run the event loop indefinitely
    loop.run_forever()
except Exception as e:
    print('Error occurred: ', e)
except KeyboardInterrupt:
    print('Program Interrupted by the user')

Просмотреть исходный код на GitHub

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

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

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

Этот код аналогичен коду из предыдущего проекта. В этой статье мы рассмотрим только те части кода, которые относятся к асинхронному веб-серверу. Поэтому, если вы ещё не знакомы с базовым сокет-сервером, прочитайте объяснение кода этого проекта.

Импорт модуля asyncio

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

import asyncio

Асинхронные функции

Функции handle_client() и blink_led() определены как асинхронные функции (определяются ключевыми словами async def), что позволяет им работать параллельно и кооперативно с другими корутинами.

Главная функция main() также определена как асинхронная функция для организации запуска сервера и других задач.

Цикл событий asyncio.get_event_loop() создается для управления и выполнения асинхронных задач.

loop = asyncio.get_event_loop()

Главная функция main() регистрируется как задача в цикле событий с помощью loop.create_task(main()), что позволяет планировать и выполнять её асинхронно.

loop.create_task(main())

Настройка асинхронного сервера

Вместо использования традиционного сокет-программирования, функция asyncio asyncio.start_server() используется для создания асинхронного TCP-сервера.

server = asyncio.start_server(handle_client, "0.0.0.0", 80)

Корутина handle_client() передается как функция обратного вызова для обработки входящих клиентских подключений.

По сути, предыдущая строка настраивает сервер, который прослушивает входящие подключения на указанном хосте и порту. Она возвращает объект, представляющий задачу сервера (server).

Когда сервер принимает новое клиентское подключение, он вызывает функцию handle_client (или любую другую корутину, указанную в качестве обработчика клиента).

Подключение клиента

Когда клиент подключается к серверу, asyncio.start_server() создает пару потоковых объектов: StreamReader для чтения данных от клиента и StreamWriter для записи данных клиенту. Узнайте больше о потоковых TCP-подключениях с asyncio.

Эти потоковые объекты (reader и writer) передаются в качестве параметров корутине, указанной в качестве обработчика клиента (handle_client в данном случае).

async def handle_client(reader, writer):

Внутри функции handle_client() объекты reader и writer используются для асинхронного чтения данных от клиента и записи данных обратно клиенту соответственно.

Объект reader (StreamReader) предоставляет методы для асинхронного чтения данных из сокетного соединения клиента (await reader.readline()).

print("Client connected")
request_line = await reader.readline()
print('Request:', request_line)

Объект writer (StreamWriter) предоставляет методы для асинхронной записи данных в сокетное соединение клиента (writer.write()).

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

Объекты reader и writer предоставляются функцией asyncio.start_server() при подключении клиента к серверу. Эти объекты позволяют серверу асинхронно взаимодействовать с клиентом, читая входящие данные и записывая исходящие данные.

Параллельное выполнение задач

Функция asyncio asyncio.create_task() используется для создания задач обработки клиентских подключений и мигания светодиодом параллельно. Это позволяет серверу принимать несколько клиентских подключений и мигать светодиодом одновременно без блокировки.

asyncio.create_task(server)
asyncio.create_task(blink_led())

Кроме того, в функции main() также есть другие асинхронные операции, использующие await asyncio.sleep() — неблокирующую версию time.sleep(). В данном случае мы просто выводим сообщение в оболочку каждые 5 секунд, но вы можете добавить любые другие задачи, которые вам нужны. При необходимости можно добавить больше операций.

while True:
    # Add other tasks that you might need to do in the loop
    await asyncio.sleep(5)
    print('This message will be printed every 5 seconds')

Цикл событий (loop.run_forever()) работает бесконечно, непрерывно обрабатывая задачи и обрабатывая клиентские подключения. Это гарантирует, что сервер остается активным и отзывчивым к входящим запросам.

loop.run_forever()

Подводя итог, мы используем асинхронные операции по всему коду, такие как чтение от клиентов await reader.readline(), отправка ответов writer.write() и переключение светодиода await asyncio.sleep().

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

Тестирование кода

Запустите предыдущий код на вашем Raspberry Pi Pico.

Тестирование кода MicroPython на плате Raspberry Pi Pico

После подключения к интернету откройте веб-браузер в той же сети и введите IP-адрес Pico для доступа к веб-серверу.

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

Базовый веб-сервер Raspberry Pi Pico на смартфоне Веб-сервер Raspberry Pi Pico получение значения

Всё это может обрабатываться одновременно с миганием светодиода и выводом сообщений в оболочку с разной частотой.

Асинхронное программирование Raspberry Pi Pico W Тестирование асинхронного веб-сервера Raspberry Pi Pico W - загрузка и запуск скрипта

Теперь вы должны понимать, что вместо мигания светодиодом и вывода сообщений вы можете запускать более сложные асинхронные задачи, и 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 при загрузке без необходимости подключения к компьютеру.

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

Заключение

В этом руководстве мы показали вам, как создать асинхронный веб-сервер на Raspberry Pi Pico с использованием модуля asyncio MicroPython. Использование асинхронного подхода позволяет Raspberry Pi Pico обрабатывать несколько клиентов одновременно и при этом выполнять другие задачи параллельно.

Мы надеемся, что это руководство было для вас полезным. Если вы только начинаете работать с веб-серверами на Raspberry Pi Pico, мы рекомендуем начать с более простого примера веб-сервера и затем перейти к асинхронному подходу.