Хочется включать свет, вентилятор или реле с телефона, не устанавливая никаких приложений? Для этого достаточно маленького веб-сервера на самой Raspberry Pi: вы открываете страницу в браузере, нажимаете кнопку, а плата управляет GPIO. Сделать такой сервер на Python можно за полчаса с помощью микрофреймворка Flask.
В статье напишем веб-интерфейс для Raspberry Pi 5: страницу с кнопками для реле и светодиода, показом температуры процессора, программным интерфейсом (API) и защитой паролем. Потом запустим сервер как службу, которая стартует вместе с платой.
Что понадобится
- Raspberry Pi 5 с системой (установка Raspberry Pi OS);
- модуль реле на GPIO27 (схема подключения есть в статье про реле);
- светодиод с резистором 330 Ом на GPIO22 (по желанию);
- компьютер или телефон в той же локальной сети.
Если реле у вас нет, оставьте только светодиод: код подойдёт и для него.
Подготовка
Создадим каталог проекта и виртуальное окружение. Параметр --system-site-packages даёт окружению доступ к библиотеке gpiozero, которая уже установлена в системе (подробнее об окружениях: venv):
mkdir -p ~/webgpio
cd ~/webgpio
python3 -m venv --system-site-packages env
source env/bin/activate
pip install flask gunicorn
В конце установки pip может вывести красную строку с ERROR:
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
types-flask-migrate 4.0 requires Flask-SQLAlchemy>=3.0.1, which is not installed.
Successfully installed flask-3.1.3 gunicorn-26.2.0 itsdangerous-2.2.0 werkzeug-3.1.9
Это предупреждение, а не ошибка. Установка прошла успешно, о чём говорит строка Successfully installed. Из-за --system-site-packages окружение видит и системные пакеты Python, а среди них есть types-flask-migrate, набор подсказок типов для Flask-Migrate, которому нужен ещё не установленный Flask-SQLAlchemy. Наше приложение не использует ни Flask-Migrate, ни SQLAlchemy, поэтому предупреждение можно пропустить. Если позже понадобится база данных, выполните pip install flask-sqlalchemy, и строка пропадёт.
Приложение Flask
Создайте файл app.py:
import hmac
import os
from functools import wraps
from flask import Flask, Response, jsonify, render_template_string, request
from gpiozero import CPUTemperature, LED, OutputDevice
app = Flask(__name__)
PASSWORD = os.environ.get("WEB_PASSWORD", "")
# реле с активным низким уровнем на GPIO27 и светодиод на GPIO22
DEVICES = {
"relay": OutputDevice(27, active_high=False, initial_value=False),
"led": LED(22),
}
cpu = CPUTemperature()
PAGE = """<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Raspberry Pi 5</title>
<style>
body { font-family: sans-serif; max-width: 420px; margin: 2em auto; padding: 0 1em; }
.row { display: flex; justify-content: space-between; align-items: center;
padding: .8em 0; border-bottom: 1px solid #ddd; }
button { font-size: 1.1em; padding: .5em 1.2em; border: 0; border-radius: 6px;
color: #fff; background: #888; cursor: pointer; }
button.on { background: #2e9e4f; }
</style>
</head>
<body>
<h1>Raspberry Pi 5</h1>
<div class="row"><span>Реле</span><button id="relay" onclick="toggle('relay')">…</button></div>
<div class="row"><span>Светодиод</span><button id="led" onclick="toggle('led')">…</button></div>
<div class="row"><span>Температура CPU</span><b id="temp">…</b></div>
<script>
function show(s) {
for (const name of ['relay', 'led']) {
const b = document.getElementById(name);
b.textContent = s[name] ? 'Включено' : 'Выключено';
b.className = s[name] ? 'on' : '';
}
document.getElementById('temp').textContent = s.cpu_temp + ' °C';
}
async function refresh() {
const r = await fetch('/api/status');
show(await r.json());
}
async function toggle(name) {
const r = await fetch('/api/' + name + '/toggle', {method: 'POST'});
show(await r.json());
}
refresh();
setInterval(refresh, 3000);
</script>
</body>
</html>
"""
def requires_auth(view):
"""Простая защита паролем (HTTP Basic). Логин любой, пароль из WEB_PASSWORD."""
@wraps(view)
def wrapper(*args, **kwargs):
auth = request.authorization
ok = (
PASSWORD
and auth is not None
and hmac.compare_digest(auth.password or "", PASSWORD)
)
if not ok:
return Response(
"Нужен пароль", 401,
{"WWW-Authenticate": 'Basic realm="Raspberry Pi"'},
)
return view(*args, **kwargs)
return wrapper
def status():
result = {name: bool(dev.value) for name, dev in DEVICES.items()}
result["cpu_temp"] = round(cpu.temperature, 1)
return result
@app.route("/")
@requires_auth
def index():
return render_template_string(PAGE)
@app.route("/api/status")
@requires_auth
def api_status():
return jsonify(status())
@app.route("/api/<name>/<action>", methods=["POST"])
@requires_auth
def api_set(name, action):
dev = DEVICES.get(name)
if dev is None or action not in ("on", "off", "toggle"):
return jsonify(error="неизвестное устройство или действие"), 404
if action == "on":
dev.on()
elif action == "off":
dev.off()
else:
dev.toggle()
return jsonify(status())
if __name__ == "__main__":
# debug=False: в режиме отладки Flask запускает программу дважды,
# и вторая копия не сможет получить доступ к тем же выводам GPIO
app.run(host="0.0.0.0", port=5000, debug=False)
Как это устроено:
- словарь
DEVICESсвязывает имена («relay», «led») с объектамиgpiozero; чтобы добавить новое устройство, добавьте строку в словарь; requires_authпроверяет пароль и возвращает ответ 401, если он неверный. Пароль берётся из переменной окруженияWEB_PASSWORD, а не из кода;- маршрут
/api/<name>/<action>принимает только запросы POST, а не GET: так случайный переход по ссылке или предзагрузка страницы браузером не включит реле; - страница обновляет состояние каждые 3 секунды и показывает температуру процессора (
CPUTemperatureизgpiozero).
Запуск и проверка
Задайте пароль и запустите:
export WEB_PASSWORD='придумайте-пароль'
python3 app.py
Flask сообщит, на каких адресах он слушает: 127.0.0.1:5000 (сама плата) и адрес платы в локальной сети. Красная строка WARNING: This is a development server относится к встроенному серверу Flask: для опытов он подходит, а для постоянной работы ниже подключим gunicorn.
Узнайте IP-адрес платы командой hostname -I и откройте в браузере на компьютере или телефоне:
http://IP-АДРЕС:5000
Браузер спросит имя пользователя и пароль: имя введите любое, пароль — тот, что задан в WEB_PASSWORD. Появятся две кнопки и температура.
То же самое можно сделать без браузера через API. Состояние:
curl -u user:ПАРОЛЬ http://IP-АДРЕС:5000/api/status
Включить реле:
curl -u user:ПАРОЛЬ -X POST http://IP-АДРЕС:5000/api/relay/on
Ответ приходит в формате JSON: {"cpu_temp": 48.7, "led": false, "relay": true}. Через такой API управлять платой могут скрипты, Home Assistant, Node-RED или Telegram-бот.
На снимке сначала запрос состояния ("relay":false), затем команда включения реле ("relay":true). Страница в браузере при следующем обновлении, через 3 секунды, показывает, что кнопка «Реле» стала зелёной.
Какие выводы меняются
Убедиться, что приложение действительно переключает линии GPIO, можно командой pinctrl -p. Она выводит все 40 контактов разъёма по порядку номеров: функцию, подтяжку и текущий уровень (расположение выводов и их назначение разобраны в статье про распиновку GPIO):
pinctrl -p
Строка читается так: op dl pn | lo // GPIO27 = output. Здесь op значит выход (output), dl и dh показывают, какой уровень выход держит (низкий drive low или высокий drive high), pn, pu и pd обозначают отсутствие, подтяжку к плюсу и подтяжку к земле, а hi и lo в конце сообщают реальный уровень на линии.
Сначала нажмём обе кнопки на странице («Включено») и вызовем pinctrl -p:
Затем выключим обе кнопки («Выключено») и повторим команду:
Между двумя снимками изменились ровно две строки, остальные контакты остались как были:
| Контакт | Линия | Устройство | Включено | Выключено |
|---|---|---|---|---|
| 13 | GPIO27 | реле | op dl pn | lo (низкий уровень) | op dh pn | hi (высокий уровень) |
| 15 | GPIO22 | светодиод | op dh pn | hi (высокий уровень) | op dl pn | lo (низкий уровень) |
У светодиода всё как обычно: включено — высокий уровень на выводе. У реле наоборот: наш модуль включается низким уровнем, поэтому в программе указано active_high=False, а на странице «Включено» соответствует lo. Если у вашего модуля реле включается высоким уровнем, уберите этот параметр.
Остальные строки в выводе (SPI, UART, I2C, GPIO7 и GPIO8 в режиме op dh) относятся к системе и нашим приложением не меняются.
Безопасность
Пароль без шифрования. HTTP Basic без HTTPS передаёт пароль открытым текстом (в кодировке base64, это не шифрование). В домашней сети это приемлемо для опытов, но не открывайте этот сервер в интернет и не пробрасывайте порт на роутере. Для доступа из любого места используйте VPN WireGuard: подключаетесь к домашней сети и открываете страницу как обычно. Не используйте такой интерфейс для управления опасными нагрузками.
Ещё несколько правил:
- пароль храните в переменной окружения или в файле с правами
chmod 600, а не в коде; - не передавайте в API произвольные номера GPIO или команды: разрешайте только заранее заданные устройства (как в словаре
DEVICES); - встроенный сервер Flask (
app.run) предназначен для разработки; для постоянной работы используйтеgunicorn, как показано дальше.
Запуск при включении платы
Запускаем через gunicorn
gunicorn — рабочий HTTP-сервер для Python-приложений. Важно запускать его с одним рабочим процессом (--workers 1): GPIO-линиями может владеть только один процесс, поэтому несколько процессов сразу будут конфликтовать. Параллельные запросы обработают потоки (--threads 4).
Проверка вручную (из каталога ~/webgpio, с активным окружением):
gunicorn --workers 1 --threads 4 --bind 0.0.0.0:8000 app:app
Если в соседнем окне ещё работает python3 app.py, остановите его сочетанием Ctrl+C: обе программы не смогут одновременно владеть линиями GPIO. Откройте http://IP-АДРЕС:8000. Браузер снова спросит имя пользователя и пароль:
После входа появится та же страница, что и раньше. В терминале gunicorn пишет, что запущен рабочий процесс типа gthread (потоки), и слушает порт 8000. Строки Handling signal: winch появляются, когда меняется размер окна терминала, их можно пропустить.
Служба systemd
Пароль положите в отдельный файл, доступный только владельцу:
sudo nano /etc/webgpio.env
WEB_PASSWORD=придумайте-пароль
sudo chmod 600 /etc/webgpio.env
Создайте файл службы:
sudo nano /etc/systemd/system/webgpio.service
[Unit]
Description=Web interface for Raspberry Pi GPIO
After=network-online.target
Wants=network-online.target
[Service]
User=pi
WorkingDirectory=/home/pi/webgpio
EnvironmentFile=/etc/webgpio.env
ExecStart=/home/pi/webgpio/env/bin/gunicorn --workers 1 --threads 4 --bind 0.0.0.0:8000 app:app
Restart=on-failure
[Install]
WantedBy=multi-user.target
Если ваш пользователь называется не pi, замените имя и пути. Включите и запустите службу:
sudo systemctl daemon-reload
sudo systemctl enable --now webgpio.service
sudo systemctl status webgpio.service
Подробнее о службах и разборе ошибок читайте в статье про автозапуск через systemd.
Что можно добавить
- Расписание: запускать включение реле по таймеру через
cronилиsystemd-таймер; - Датчики: показывать температуру и влажность с DHT22 на той же странице;
- Управление сервоприводом: ползунок угла для сервопривода;
- Датчик движения: статус PIR-датчика на странице;
- HTTPS: поставить перед приложением обратный прокси Nginx, как в статье про LEMP, и получить сертификат.
Если что-то не так
| Симптом | Что проверить |
|---|---|
| Браузер не открывает страницу | IP платы, порт (5000 для app.py, 8000 для gunicorn), что компьютер в той же сети |
| Всегда 401 «Нужен пароль» | Переменная WEB_PASSWORD не задана (пустой пароль не принимается) или пароль введён неверно |
GPIOPinInUse или Device or resource busy | Другая копия программы уже владеет линией: завершите её (pgrep -af app.py) или остановите службу |
| Служба не запускается | journalctl -u webgpio -e: путь к gunicorn, имя пользователя, права на /etc/webgpio.env |
| Кнопка работает, но реле не включается | Проверьте схему и логику модуля (реле на Raspberry Pi 5) |
Что дальше
- Реле на Raspberry Pi 5 — подключение и безопасность;
- Node-RED на Raspberry Pi 5 — визуальные сценарии поверх такого API;
- Telegram-бот на Raspberry Pi — управление командами из мессенджера.
Материалы
- Документация Flask: flask.palletsprojects.com
- Документация gunicorn: docs.gunicorn.org
- Документация gpiozero: gpiozero.readthedocs.io











