Привет, Хабр.

Сегодня в большом числе проектов домашней (и не только) автоматизации используется Raspberry Pi. При этом достаточно удобно иметь не только прямой доступ к устройству, но и использовать браузер — это позволяет выполнять необходимые действия и с компьютера, и с мобильного телефона, и даже удаленно из любой точки мира.



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

Статья расчитана для начинающих, профи вряд ли найдут здесь что-то кардинально новое, ну а новичкам в Linux надеюсь, будет полезно. Для тех кому интересно, продолжение под катом.

Примечание: эта статья является своего рода «экспериментом», как-то в комментариях жаловались что на Хабре недостаточно статей для начинающих. Я попытался восполнить пробел, ну а по оценкам будет видно, имеет смысл продолжать в таком формате или нет.

Итак, приступим.

Настройка Raspberry Pi


Будем надеятся, что у читателя есть Raspberry Pi, которая подключена к домашней сети через WiFi или Ethernet, и читатель знает что такое IP адрес и как зайти удаленно на Raspberry Pi через SSH при помощи putty. Мы будем рассматривать так называемую headless-конфигурацию — без клавиатуры и монитора. Но перед тем, как делать что-то с Raspberry Pi, пара небольших лайфхаков.

Совет N1. Чтобы что-то удаленно делать с Raspberry Pi, на нем нужно настроить SSH, а по умолчанию он выключен. Можно пойти традиционным способом, и запустить стандартный конфигуратор, но можно сделать проще — после записи образа диска достаточно создать пустой файл ssh (без расширения) в корне SD-карты. Дальше, после загрузки Raspberry Pi, SSH будет сразу активен.

Чтобы зайти удаленно на устройство, нужно узнать IP-адрес Raspberry Pi. Для этого достаточно открыть контрольную панель своего маршрутизатора, найти там список DHCP-клиентов, скопировать оттуда нужный IP-адрес (например, это будет 192.168.1.102), и ввести команду putty.exe pi@192.168.1.102 (для Windows) или ssh pi@192.168.1.102 для Linux или OSX.

Однако, IP-адреса могут меняться, например после перезагрузки маршрутизатора, это не всегда удобно. Из этого следует Совет N2 — настроить статический IP-адрес. Для этого на Raspberry Pi выполняем команду sudo nano /etc/dhcpcd.conf, и вводим следующие настройки:

interface eth0
static ip_address=192.168.1.152/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1 8.8.8.8

Если нужен адрес WiFi, то интерфейс будет wlan0, если Ethernet то eth0. IP-адреса разумеется, нужно тоже подставить свои. После перезагрузки убеждаемся что IP-адрес правильный, введя команду ifconfig.

Теперь все готово, можем приступать к Python. Все примеры даны для Python 3.7, т.к 2.7 уже давно устарел, и поддерживать его бесмысленно. Но при небольших изменениях кода все заработает и там, если нужно. Кстати, язык Python является кроссплатформенным — это значит что весь приведенный ниже код можно запустить и на Windows и на OSX, ну и разумеется, на Raspberry Pi. Из этого следует Совет N3 — отлаживать программу можно и на обычном ПК, а уже готовую версию заливать на Raspberry Pi. Возможно, придется лишь сделать функции-обертки для методов GPIO, все остальное будет работать.

Итак, наша задача — обеспечить доступ к приложению через обычный браузер. Ибо это стильно-модно-молодежно, ну и «интернет вещей» это наше все.

Способ 1: командная строка


Самый простой способ, не требующий вообще никакого программирования.

Выбираем нужную папку на Raspberry Pi, и вводим команду:

python3 -m http.server 5000 

Все, на Raspberry Pi работает файловый сервер! Достаточно зайти на страницу http://192.168.1.102:5000 и мы увидим наши файлы в браузере:



Это достаточно удобно, если нужно открыть удаленный доступ к каким-либо файлам с минимумом затраченных сил. Можно также ввести команду sudo python3 -m http.server 80 и запустить сервер со стандартным 80-м портом, это позволит не указывать порт в адресной строке браузера.

Кстати, если мы хотим, чтобы сервер работал и после закрытия терминала, можно использовать команду sudo nohup python3 -m http.server 80 & — это запустит процесс в фоне. Убить такую программу можно перезагрузкой, или вводом в командной строке команды sudo killall python3.

Способ 2: SimpleHTTPServer


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

import http.server
import socketserver
from threading import Thread
import os

def server_thread(port):
    handler = http.server.SimpleHTTPRequestHandler
    with socketserver.TCPServer(("", port), handler) as httpd:
        httpd.serve_forever()

if __name__ == '__main__':

    port = 8000
    print("Starting server at port %d" % port)
    os.chdir("/home/pi/Documents")
    Thread(target=server_thread, args=(port,)).start()

Команда os.chdir является опциональной, если мы хотим предоставить доступ из сервера к какой-то другой папке, кроме текущей.

Способ 3: HTTPServer


Это уже полноценный web-сервер, способный обрабатывать GET и POST-запросы, возвращать разные данные и пр. Но и кода разумеется, понадобится больше.

Рассмотрим минимально работающий вариант сервера:

from http.server import BaseHTTPRequestHandler, HTTPServer

html = "<html><body>Hello from the Raspberry Pi</body></html>"

class ServerHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/":
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(html.encode('utf-8'))
        else:
            self.send_error(404, "Page Not Found {}".format(self.path))

def server_thread(port):
    server_address = ('', port)
    httpd = HTTPServer(server_address, ServerHandler)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()

if __name__ == '__main__':
    port = 8000
    print("Starting server at port %d" % port)
    server_thread(port)

Запускаем браузер, и видим в нем нашу HTML-страницу:



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

Добавим в HTML тег img:

html = '<html><body><h3>Hello from the Raspberry Pi</h3><img src="raspberrypi.jpg"/></body></html>'

Исходный файл «raspberrypi.jpg» разумеется, должен лежать в папке с программой. Добавим в функцию do_GET возможность получения файлов:

    def do_GET(self):
        print("GET request, Path:", self.path)
        if self.path == "/":
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(html.encode('utf-8'))
        elif self.path.endswith(".jpg"):
            self.send_response(200)
            self.send_header('Content-type', 'image/jpg')
            self.end_headers()
            with open(os.curdir + os.sep + self.path, 'rb') as file:
                self.wfile.write(file.read())
        else:
            self.send_error(404, "Page Not Found {}".format(self.path))

Запускаем сервер, и видим соответствующую картинку:



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

import psutil
import json

def cpu_temperature():
    return psutil.sensors_temperatures()['cpu-thermal'][0].current

def disk_space():
    st = psutil.disk_usage(".")
    return st.free, st.total

def cpu_load() -> int:
    return int(psutil.cpu_percent())

def ram_usage() -> int:
    return int(psutil.virtual_memory().percent)

def do_GET(self):
        ...
        elif self.path == "/status":
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            health = {'CPUTemp': cpu_temperature(), 'CPULoad': cpu_load(), "DiskFree": disk_space()[0], "DiskTotal": disk_space()[1], "RAMUse": ram_usage()}
            self.wfile.write(json.dumps(health).encode('utf-8'))

Теперь мы можем открыть в браузере ссылку http://192.168.1.102:5000/status и увидеть текущие параметры системы:



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

Заключение


Все задуманное в одну часть не влезло. Если продолжение будет аудитории интересно, в следующей части я расскажу об интерактивном сервере с Javascript и об использовании flask.

Важно: меры безопасности

Если для Raspberry Pi будет использоваться внешний IP-адрес, обязательно стоит помнить о мерах безопасности. Может показаться что ваш мини-сервер никому не нужен, однако сейчас не составляет труда пакетно просканировать все диапазоны IP-адресов (как пример, Украина, Австрия) и найти все доступные устройства. Так что обязательно стоит поменять пароль на Raspberry Pi, и не стоит хранить на устройстве какую-либо конфиденциальную информацию (папки Dropbox, имена/пароли захардкоженные в скриптах, фото и пр).

PS: Для понимания картины добавил опрос

Комментарии (40)


  1. gudvinr
    18.10.2019 23:33
    +2

    А где остальные 4?


    По сути всё, что вы делаете тут — это запускаете стандартный http.server одним способом. Я уж грешным делом подумал, что тут и systemd будет, и nginx unit, и синие киты.


  1. nochkin
    18.10.2019 23:44
    +1

    Странный совет про статический IP.
    То есть, если дать ему 102 в данном случае, а потом другое устройство получит 102 по DHCP, то как получить доступ?
    Статический лучше выдавать за пределами DHCP диапазона.

    P.S.: Я обычно стараюсь таблицу статических адресов держать на самом маршрутизаторе, что бы было в одном месте, но в данном случае это уже не так важно.


    1. DmitrySpb79 Автор
      18.10.2019 23:50

      Да, согласен, спасибо за уточнение.


  1. Ivanii
    18.10.2019 23:59

    В разных ОС по разному устанавливаются и запускаются службы, во многих компоненты можно выбрать при установке, я так и не понял по какую ОС статья.


    1. DmitrySpb79 Автор
      19.10.2019 00:19

      Приведенный код является кроссплатформенным, должно работать везде.


  1. Laimerus0073
    19.10.2019 07:30
    +3

    Не советую в конфигах устройства прописывать статический адрес.
    Гораздо правильнее будет в роутере создать статическую привязку «MAC address = IP Adress».
    По какой-либо причине изменится IP пространство на интерфейсе устройства ( подсеть ) — будет будет увлекательный квест по получению доступа к ней.


    1. DmitrySpb79 Автор
      19.10.2019 12:06

      Мой копророутер таких настроек вроде не имеет, но в целом согласен, идея хорошая.


  1. BoogieMan75
    19.10.2019 09:10

    еще один туториал ни о чем?


  1. Meklon
    19.10.2019 10:50
    +1

    Круто. Спасибо большое.


  1. apapacy
    19.10.2019 11:42

    Есть вопрос по стабильности. Если нужно чтобы http был доступен 24х7 хватит и такой реализации? Что будбет если устройство начнут буртфорсить?


    1. DmitrySpb79 Автор
      19.10.2019 12:04

      Многолетней статистики у меня нет.

      2-3 месяца домашний мини-сервер у меня как-то работал, дольше нужды не было. Электронные часы на Raspberry Pi проработали пару лет, потом сдохла SD-карта (хотя её можно было бы сделать read-only, тогда проблем бы не было).

      Если задачи ресурсоемкие, для Raspberry Pi обязателен хороший блок питания и радиатор на проц, иначе виснет от перегрева.


      1. Antonto
        19.10.2019 23:52

        Добавлю, что есть дополнительные модули с бесперебойником полноценным.


  1. sabirovrinat85
    19.10.2019 11:53
    +1

    не советую использовать Google DNS в качестве резервного, многие ресурсы подолгу остаются недоступными после изменений в зонах, особенно часто вообще не резолвятся в случае добавления субдоменов, гораздо лучше работают DNS сервера от Yandex


    1. DmitrySpb79 Автор
      19.10.2019 11:56

      Спасибо. Ни разу их не использовал, буду иметь в виду.


  1. iig
    19.10.2019 12:40

    Самое интересное — включение самоделки в автозагрузку, назначение прав, запись/незапись логов… про это ни слова.
    Что произойдет, если внутри есть некий неатомарный ногодрыг (опрос внешнего датчика к примеру) и веб-сервер при обработке 2 почти одновременно пришедших запросов выполнит эти действия параллельно в 2 потоках? Это же raspberry, туда обычно подключают разную периферию


    1. DmitrySpb79 Автор
      19.10.2019 13:17

      Если у аудитории будет интерес, можно дойти и до многопоточности. Но по рейтингу оценок… сомневаюсь что до этого дойдет. Со статьями про телефонных мошенников, штурм немецкого бункера или украинца Незнайку мне конкурировать без шансов :)


      1. Areso
        19.10.2019 14:27

        Просто уровень статьи вызывает вопросы.
        Кому-то и этого много, кому-то этого откровенно мало (вопросы для рассмотрения iig поддержу).
        К примеру, в статье нет примеров с WSGI, который считается более «зрелым» методом, потому что все проблемы с безопасностью, брутфорсом и тому подобным берут на себя решения на базе Apache/Nginx веб-серверов, а не решения для разработчиков на базе Питона, где вопросы с безопасностью выставления такого в Интернет вообще решено было оставить за скобками статьи.


        1. DmitrySpb79 Автор
          19.10.2019 15:23
          +1

          Просто уровень статьи вызывает вопросы

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


          1. Areso
            19.10.2019 15:33

            Про добавление в автозагрузку стоит добавить прямо в эту статью.

            Потому что если представить себе, что RPi купил обычный человек (имевший опыт только с Windows), то работа с Cron'ом или тем более systemd для добавления старта своей программы по target'у потребует гуглежа и многочисленных неудачных попыток.


            1. DmitrySpb79 Автор
              19.10.2019 15:40

              Так можно и без systemd для начала обойтись, просто вписать команду в /etc/rc.local. Ничуть не сложнее, чем когда-то был autoexec.bat (если кто помнит:).


              1. osmanpasha
                19.10.2019 21:41

                Просто вписать можно, но не факт, что все заработает, так как окружение при запуске из rc.local совсем другое. А если ваш скрипт из rc.local запустится до того, как сетевые интерфейсы поднимутся? И текущая папка другая будет? И пользователя хорошо бы сменить, чтобы от рута не запускать ваш сервер. Короче, дьявол в детялах, и, кстати, по-моему, с systemd это как раз можно сделать унифицированно, без необходимости городить свои велосипеды.


                1. DmitrySpb79 Автор
                  19.10.2019 22:09

                  Да, в systemd можно указать явно, после какого сервиса стартовать, но если не ошибаюсь, для rc.local это тоже прописано в системе. Root обычно по-любому нужен если нужен GPIO и работа с периферией (либо надо разделять сервисы на 2 разных).

                  Есть краткий туториал по разным способам запуска тут кстати: www.dexterindustries.com/howto/run-a-program-on-your-raspberry-pi-at-startup


                  1. maledog
                    21.10.2019 17:51

                    Root обычно по-любому нужен если нужен GPIO и работа с периферией (либо надо разделять сервисы на 2 разных).

                    Не обязателен, обычно по-умолчанию в таких дистрибутивах выделяются специальные группы gpio,i2c,spi… И даже в том случае, если ваша программа работает напрямую с памятью, то часть ее все равно выделена в /dev/gpiomem на которую даются права пользователям из группы gpio.


                    1. DmitrySpb79 Автор
                      21.10.2019 18:03

                      Да, спасибо за уточнение. Раньше GPIO на Raspbian без root не работал, в последних версиях вроде должно, но не проверял.
                      raspberrypi.stackexchange.com/questions/40105/access-gpio-pins-without-root-no-access-to-dev-mem-try-running-as-root


          1. PashaWNN
            21.10.2019 19:54

            Просто, как мне кажется, для тех, кто ещё совсем ничего на Raspberry Pi не делал, рановато читать статью, т. к. поднимать http-сервер на python это не первое, что будет нужно (да и в самой статье про это сказано: «у нас уже есть супер Python-программа, делающая что-то очень важное»), а для тех, кто эту супер Python-программу, делающую важное, уже написал — воспользоваться стандартной библиотекой http не составит труда и без статьи.


            1. DmitrySpb79 Автор
              21.10.2019 20:00

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

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


      1. u_235
        19.10.2019 15:08
        +2

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


        1. iig
          19.10.2019 18:17

          В статье ожидается увидеть что-то интересное. Либо из теории, либо необычное know-how, либо изящное решение практической задачи. Но helloworld это слабовато.


          1. DmitrySpb79 Автор
            19.10.2019 19:46

            Туториалы Hello world для начинающих тоже нужны.
            Но да, как я и написал в самом начале, это был эксперимент, посмотреть по оценкам насколько такие статьи востребованы. Нет значит нет, не вопрос, буду смотреть статистику в конце 3х дней голосования.

            Кстати скажу по секрету, что недовольные, ставящие минусы, есть везде, независимо от уровня статьи, хоть про hello world, хоть про нейронные сети, хоть про декодирование RDS.


      1. Gumrak
        19.10.2019 19:36

        Интересно в основном для рид онли участников, имхо. Но в формировании рейтинга таковые не участвуют.


        1. Areso
          20.10.2019 18:40

          По количеству закладок под статьей можно косвенно судить, насколько статья полезна, в том числе учитывая RC/RO участников.


  1. Coocos
    19.10.2019 13:48

    Очень содержательная статья. Нет конечно. Название нужно поменять на «Как запустить python на linux».


  1. arthuriantech
    19.10.2019 18:00
    +1

    Для быстрого и энергоэффективного HTTP-сервера лучше взять uWSGI — он может без проблем смотреть "в мир" и обладает огромной гибкостью конфигурации, а в случае необходимости можно подключить один из WSGI-совместимых фреймворков. Его также можно подружить с aiohttp поверх uvloop — получите удобный и высококонкуррентный сервер на современных async/await, а uWSGI будет просто выступать мастер-процессом, контролирующим рабочие процессы.


  1. zoldaten
    19.10.2019 18:04

    sudo ifdown wlan0 & sudo ifup wlan0 — поднять сеть без перезагрузки.

    А так, интересно было бы еще узнать, какую нагрузку (сколько клиентов) выдержат сервера.


  1. maksim_R
    20.10.2019 09:01
    +1

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

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


    1. DmitrySpb79 Автор
      20.10.2019 19:41

      Спасибо за поддержку. Именно такая цель и ставилась, обойтись по возможности минимумом кода без лишних функций.


  1. dio_eraclea
    20.10.2019 23:09

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


    1. DmitrySpb79 Автор
      20.10.2019 23:12

      Да, верно. Ищите в гугле по запросу «проброс портов», как вывести сервер наружу.


      1. iig
        21.10.2019 07:08
        +2

        +DDNS не забудьте.


      1. maledog
        21.10.2019 18:07

        Не поможет. Большая часть провайдеров сейчас использует NAT и давно уже не выделяет белые ip обычным пользователям. Да и поделка не вынесет жизни в инете. Или сломают или заддосят. Хватит одного скрипта, который в несколько потоков начнет подбирать пароль, которого здесь и нет.
        По моему мнению, если это планируется выставлять в инет, то нужно:
        1. Снять за пару баксов в месяц VPS. И разместить на нем web интерфейс, сервер MQTT и VPN-сервер.
        2. Клиентскую часть на raspberry pi реализовать в виде клиента mqtt. vpn поднимать при подъеме сети.
        Если устройство одно в своем роде, то mqtt-сервер можно оставить на стороне raspberry pi, тогда можно будет написать приложение для android для руления по локальной сети без интернета.
        Возможен пункт один без vpn, но тогда придется еще и с шифрованием mqtt заморочиться. И vpn удобнее в плане доступа к малине для диагностики.