5 способов сделать Python-сервер на Raspberry Pi. Часть 2

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

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



Статья рассчитана для начинающих.

Перед началом пара примечаний.

Во-первых, я и сам сомневался, стоит ли делать продолжение и ожидал больший поток критики и низких оценок, но как показал опрос в первой части, 85% читателей нашли приведенную там информацию полезной. Понимаю, что некоторых профи статьи «для чайников» раздражают, но все когда-то начинали, так что придется потерпеть.

Во-вторых, я буду писать про программирование, а не про администрирование. Так что вопросы настройки Raspbian, конфигов, VPN, безопасности и прочего, здесь рассматриваться не будут. Хотя это тоже важно, но нельзя объять необъятное. Здесь будет только про Python, и как сделать сервер на нем.

Кому все это неинтересно, могут нажать кнопку back в браузере прямо сейчас и не тратить свое ценное время ;)

А мы приступим.

Напомню, в предыдущей части мы закончили на том, что запустили на Raspberry Pi простой веб-сервер, показывающий статическую страницу:

image

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

Подготовка


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

try:
    import RPi.GPIO as GPIO
except ModuleNotFoundError:
    pass

led_pin = 21

def raspberrypi_init():
    try:
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(led_pin, GPIO.OUT)
    except:
        pass

def rasperrypi_pinout(pin: int, value: bool):
    print("LED ON" if value else "LED OFF")
    try:
        GPIO.output(pin, value)
    except:
        pass

def rasperrypi_cleanup():
    try:
        GPIO.cleanup()
    except:
        pass

Как можно видеть, каждая функция обращения к GPIO «обернута» в блок try-catch. Зачем это сделано? Это позволяет отлаживать сервер на любом ПК, включая Windows, что достаточно удобно. Теперь мы можем вставить эти функции в код веб-сервера.

Наша задача — добавить на веб-страницу кнопки, позволяющие из браузера управлять светодиодом. Будут рассмотрены 3 способа реализации.

Способ 1: Неправильный


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

Создадим строку с HTML-страницей.

html = '''<html>
              <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;}
                 .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}
              </style>
              <body>
                 <h2>Hello from the Raspberry Pi!</h2>
                 <p><a href="/led/on"><button class="button button_led">Led ON</button></a></p>
                 <p><a href="/led/off"><button class="button button_led">Led OFF</button></a></p>
              </body>
            </html>'''

Здесь можно отметить 3 момента:

  • Мы используем CSS для указания стиля кнопок. Это можно было бы и не делать и обойтись всего 4 строчками HTML-кода, но тогда наша страница выглядела бы как «привет из 90х»:

  • Для каждой кнопки мы создаем локальную ссылку типа /led/on и /led/off
  • Смешивать ресурсы и код это плохой стиль программирования, и в идеале, HTML лучше хранить отдельно от кода на Python. Но моя цель — показать минимально работающий код, в котором минимум лишнего, так что некоторые вещи для простоты опущены. К тому же, это удобно, когда код можно просто скопировать из статьи, без лишней возни с дополнительными файлами.

Сам сервер мы уже рассматривали в предыдущей части, осталось добавить в него обработку строк '/led/on' и '/led/off'. Обновленный код целиком:

from http.server import BaseHTTPRequestHandler, HTTPServer

class ServerHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        print("GET request, Path:", self.path)
        if self.path == "/" or self.path.endswith("/led/on") or self.path.endswith("/led/off"):
            if self.path.endswith("/led/on"):
                rasperrypi_pinout(led_pin, True)
            if self.path.endswith("/led/off"):
                rasperrypi_pinout(led_pin, False)
            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)

    raspberrypi_init()

    server_thread(port)

    rasperrypi_cleanup()

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



Тестировать сервер можно не только на Raspberry Pi, но и на Windows или OSX, в консоли будут сообщения LED ON, LED OFF при нажатии на соответствующую кнопку:



Теперь выясним, чем же этот способ плох, и почему он «неправильный». Этот пример вполне рабочий, и довольно часто копируется в разных туториалах. Но проблем тут две — во-первых, это неправильно перезагружать страницу целиком, когда мы лишь хотим зажечь светодиод. Но это еще полпроблемы. Вторая, и более серьезная, проблема в том, что когда мы нажимаем кнопку включения светодиода, адрес страницы становится http://192.168.1.106:8000/led/on. Браузеры обычно запоминают последнюю открытую страницу, и при последующем открытии браузера команда включения светодиода сработает еще раз, даже если мы этого не хотели. Поэтому мы перейдем к следующему, более правильному способу.

Способ 2: Правильный


Чтобы сделать все правильно, вынесем функции включения и выключения светодиода в отдельные запросы, а вызывать их будем асинхронно с помощью Javascript. Код HTML страницы теперь будет выглядеть так:

html = '''<html>
              <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;}
                 .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}
              </style>
              <script type="text/javascript" charset="utf-8">
                    function httpGetAsync(method, callback) {
                        var xmlHttp = new XMLHttpRequest();
                        xmlHttp.onreadystatechange = function() { 
                            if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
                                callback(xmlHttp.responseText);
                        }
                        xmlHttp.open("GET", window.location.href + method, true);
                        xmlHttp.send(null);
                    }
    
                    function ledOn() {
                        console.log("Led ON...");
                        httpGetAsync("led/on", function(){ console.log("Done"); });
                    }
    
                    function ledOff() {
                        console.log("Led OFF...");
                        httpGetAsync("led/off", function(){ console.log("Done"); });
                    }                            
              </script>
              <body>
                 <h2>Hello from the Raspberry Pi!</h2>
                 <p><button class="button button_led" onclick="ledOn();">Led ON</button></p>
                 <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p>
              </body>
            </html>'''

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

Теперь осталось добавить на сервер обработку 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 == "/led/on":
            self.send_response(200)
            self.send_header('Content-type', 'text/plain')
            self.end_headers()
            rasperrypi_pinout(led_pin, True)
            self.wfile.write(b"OK")
        elif self.path == "/led/off":
            self.send_response(200)
            self.send_header('Content-type', 'text/plain')
            self.end_headers()
            rasperrypi_pinout(led_pin, False)
            self.wfile.write(b"OK")
        else:
            self.send_error(404, "Page Not Found {}".format(self.path))

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

Способ 3: Более правильный


Вроде бы все уже работает. Но разумеется, приведенный код можно (и нужно) улучшить. Дело в том, что для управления светодиодом мы используем GET-запросы. Это экономит нам место в коде, но методологически это не совсем правильно — GET-запросы предназначены для чтения данных с сервера, они могут кешироваться браузером, и вообще говоря, не должны использоваться для изменения данных. Правильный способ — это использовать POST (для тех, кому интересны детали, подробнее тут).

Поменяем вызовы в HTML с get на post, ну а заодно, раз уж код у нас асинхронный, выведем статус ожидания ответа сервера и отображения результатов работы. Для локальной сети это заметно не будет, но для медленного соединения весьма удобно. Чтобы было интереснее, для передачи параметров будем использовать JSON.

Окончательный вариант выглядит так:

html = '''<html>
              <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;}
                 .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}
              </style>
              <script type="text/javascript" charset="utf-8">
                    function httpPostAsync(method, params, callback) {
                        var xmlHttp = new XMLHttpRequest();
                        xmlHttp.onreadystatechange = function() { 
                            if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
                                callback(xmlHttp.responseText);
                            else
                                callback(`Error ${xmlHttp.status}`)
                        }
                        xmlHttp.open("POST", window.location.href + method, true);
                        xmlHttp.setRequestHeader("Content-Type", "application/json");
                        xmlHttp.send(params);
                    }

                    function ledOn() {
                        document.getElementById("textstatus").textContent = "Making LED on...";
                        httpPostAsync("led", JSON.stringify({ "on": true }), function(resp) { 
                            document.getElementById("textstatus").textContent = `Led ON: ${resp}`;
                        });
                    }

                    function ledOff() {
                        document.getElementById("textstatus").textContent = "Making LED off...";
                        httpPostAsync("led", JSON.stringify({ "on": false }), function(resp) { 
                            document.getElementById("textstatus").textContent = `Led OFF: ${resp}`;
                        });
                    }                            
              </script>
              <body>
                 <h2>Hello from the Raspberry Pi!</h2>
                 <p><button class="button button_led" onclick="ledOn();">Led ON</button></p>
                 <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p>
                 <span id="textstatus">Status: Ready</span>
              </body>
            </html>'''

Добавим в сервер поддержку GET и POST запросов:

import json

class ServerHandler(BaseHTTPRequestHandler):
    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'))
        else:
            self.send_error(404, "Page Not Found {}".format(self.path))

    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        body = self.rfile.read(content_length)
        try:
            print("POST request, path:", self.path, "body:", body.decode('utf-8'))
            if self.path == "/led":
                data_dict = json.loads(body.decode('utf-8'))
                if 'on' in data_dict:
                    rasperrypi_pinout(led_pin, data_dict['on'])

                self.send_response(200)
                self.send_header('Content-type', 'text/plain')
                self.end_headers()
                self.wfile.write(b"OK")
            else:
                self.send_response(400, 'Bad Request: Method does not exist')
                self.send_header('Content-Type', 'application/json')
                self.end_headers()
        except Exception as err:
            print("do_POST exception: %s" % str(err))

Как можно видеть, мы теперь используем одну функцию led, в которую с помощью json передается параметр «on», принимающий True или False (при вызове в HTML передается соответственно json-строка вида { «on»: true }). Также стоит обратить внимание на try-catch — это блокирует сервер от «падения», например, если кто-то пошлет строку с невалидным json на сервер.

Если все было сделано правильно, мы получаем сервер с обратной связью, который должен выглядеть примерно так:



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

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

Заключение


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

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

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

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

Всем удачных экспериментов.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 35

    0

    простите за оффтоп, опус чисто посмеяться. Так совпало, что сегодня решил приладить лежащую в столе малинку под принт сервер для USB принтера. Накатил raspbian+cups+samba. Дело нехитрое, почти все конфиги на дефолте. Принтер традиционный HP LaserJet 1020. Вобщем сюрпризов не ожидалось. Однако ж…


    1) первая группа компов на win10 — малину видно по шаре, запросило логин-пасс, ввел, подошло, открыл шару принтеров, устанавливаю принтер — после выбора драйвера пишет нет связи с принтером, попробовал еще и еще… опа, сработало. Чудеса! Причем на двух машинах не с первого раза. Ладно, думаю, бывает, главное встало.


    2) Едем дальше: windows server 2019 — шара ни по имени ни по ip не открывается, пишет сетевой путь не найден. Хотя в проводнике в нетворке малинка отображается как комп! Ну и ладно, не больно-то и хотелось с сервера печатать, всё равно у меня он типо NAS безлюдный.


    3) Пришла очередь win7 — шару видно, заходим в принтеры — логин пасс не спрашивало вообще, но при этом открыло. Пробуем установить принтер — ошибка, нет связи с принтером. И магия третьей попытки уже не срабатывает, и даже с десятой не помогло. Пичалька.


    Вобщем мораль сей басни такова… жись — боль. А скрещивание ежа с ужом да еще и на арме — боль вдвойне )


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

      +3
      По моему скромному опыту (я все же не админ) с шарами в Windows всегда геморрой :)
      +2
      Ух и накрутили… А чем плох способ с отправкой формы и методом PUT? Он работает одинаково на всех устройствах и не требует поддержки JS (У нас ведь IoT, верно? А на фоторамке работать будет?)
      Немного кода…
      <form action="/api/switch_light" method="put" id="disable">
      	<input type=hidden name=light value=0 />
      </form>
      <form action="/api/switch_light/" method="put" id="enable">
      	<input type=hidden name=light value=1 />
      </form>
      <button type="submit" form="disable" value="Submit">On</button>
      <button type="submit" form="enable" value="Submit">Off</button>



      P. S.: Практичней и наглядней было бы взять Flask/CherryPy в качестве веб-сервера.
        +1
        Спасибо, с формой тоже возможный вариант.
          0

          PUT не поддерживается для HTML-форм.
          https://developer.mozilla.org/ru/docs/Web/HTML/Element/form#attr-method

            +3
            Не задумывался о поддержке соответствующего атрибута.

            Тогда POST, но уж точно не GET, иначе любой поисковой бот который случайно наткнется на веб-страницу будет включать/выключать свет.
            0

            В таком простом проекте даже bottle.py сойдёт. А как только перестанет хватать (что вряд-ли) на flask всего несколько минут мигрировать.
            И читать проще код будет, и функционала больше

              0

              Там автор про асинхронность затеял… Тогда aiohttp уж надо было брать. Уж куда компактнее и прозрачнее получилось бы.

                0
                Асинхронность тут только на стороне Javascript. А asyncio для начинающих вообще штука мрачная, лучше и не пробовать :)
            0
            А при заходе на страничку такую можно выводить состояние светодиода? Я понимаю, что сервер может хранить где-то флажок, но можно ли считать состояние пина и его уже вернуть?
            –1
            напоминает картинку с хлебом и автобусом, вряд ли что-то серьезнее hello world'а на джанге будет работать нормально
              0
              Будет. Но БД надо размещать на другом сервере, или использовать in-memory. Долгое время на raspberry pi использовал Asterisk (до 4-х конкурентных звонков) и пара WSGI-приложений.

              Нельзя размещать приложения требовательные к:
              — сети (реализована поверх usb)
              — IO (microSD)
              — времени (нет хардварных часов в комплекте)

              А так ARM — вполне годится в качестве домашней тестовой площадки.
              +1

              Вместо ваших подавляющих ошибки try-except'ов


              try:
                  import RPi.GPIO as GPIO
              except ModuleNotFoundError:
                  pass

              можно использовать куда более элегантные конструкции из стандартной библиотеки:


              with contextlib.suppress(ModuleNotFoundError):
                  import RPi.GPIO as GPIO
              
              def rasperrypi_cleanup():
                  with contextlib.suppress(Exception):
                      GPIO.cleanup()

              Хотя я бы, конечно, сделал класс-адаптер для работы с аппаратной частью и при ошибке инициализации создавал экземпляр мокапа для отладочных целей с соответствующим ворнингом:


              class RPiDrvAbstract:
                  def __init__(self, led_pin: int = 21):
                      self.led_pin = led_pin
              
                  def init(self):
                      log.debug('GPIO init')
              
                  def pin_out(self, pin: int, value: bool)
                      log.debug(f'GPIO pin #{pin} set to {value}')
              
                  def claenup(self):
                      log.debug('GPIO cleanup')
              
              class RPiDrv(RPiDrvAbstract):
                  def __init__(self, *av, **kw):
                      super().__init__(*av, **kw)
                      import RPi.GPIO as GPIO
                      self.GPIO = GPIO
              
                  def init(self):
                      super().init()
                      self.GPIO.setmode(GPIO.BCM)
                      self.GPIO.setup(led_pin, GPIO.OUT)
              
                  def rasperrypi_pinout(pin: int, value: bool):
                      super().pin_out(pin, value)
                      self.GPIO.output(pin, value)
              
                  def cleanup(self):
                      super().cleanup()
                      self.GPIO.cleanup()
              
              try:
                  drv = RPiDrv()
                  drv.init()
              except Exception as e:
                  log.warninig(f'RPi initialization error {e}. Mockup used.')
                  drv = RPiDrvAbstract()
                  drv.init()
                0
                Спасибо, хороший вариант.
                0
                Неплохо бы в назваии статьи (в первой части тоже) указывать какой именно сервер вы тут поднимаете, их ведь много хороших и разных
                  0
                  Здесь не рассматривается запуск готового сервера, используется свой собственный на Python.
                    0
                    Я это понял, но какой сервер вы пишете на Python? Веб? БД? Терминалов? Этого из заголовка неясно, вот я о чем
                  0
                  Спасибо за статью.
                  Со светодиодами понятно, они уже пропитаны Дмитриями Осиповыми.
                  Напишите про диммер-полоску для светодиода. Для многих будет актуально. Как плавно гасить и включать с веб-страницы?
                    0
                    Дайте ссылку, что за полоска такая.

                    Если обычная светодиодная лента — смотрите в сторону ШИМ, поддержка есть в Python. Примерное описание есть здесь: medium.com/@danidudas/how-to-connect-rgb-strip-led-lights-to-raspberry-pi-zero-w-and-control-from-node-js-70ddfec19f0b
                      0
                      Не совсем лента, но тоже PWM. Есть рука —
                      и есть ее проект на github
                      Управление с web-страницы бегунками. Метод GET, как понимаю (судя по видео и коду).
                      Хотелось бы услышать ваше экспертное мнение, хорош ли GET в данном случае для управления бегунками или есть более рациональный вариант.
                        0
                        Методически правильнее POST, да. По стандарту GET должен использоваться только для чтения: www.w3.org/Protocols/rfc2616/rfc2616-sec9.html

                        Но это не значит, что многие так не делают :)
                          0
                          Методы одинаково быстро работают? И не будет ли здесь уместнее java? Проблема в том, что при быстрым взаимодействии (поркутить сразу несколько бегунков с короткими паузами), в том числе при выводе на эту же страницу видео с gstreamer, рука начинает откровенно подвисать и потом либо «нагоняет» упущенное потоком движений либо стопорится.

                          *управление RGB в статье — артиллерия по воробьям. Сейчас rgb идут с ик-модулями, командам с пульта к которым можно обучить rm-mini например и управлять удаленно без rasberry. **А pigpio daemon (в статье о нем) еще тот фрукт.
                            0
                            Возможно, паузы из-за того, что Flask настроен на работу в однопоточном режиме.

                            stackoverflow.com/questions/14814201/can-i-serve-multiple-clients-using-just-flask-app-run-as-standalone

                            Хотя возможно это было сделано специально, если PCA9685_pwm не поддерживает многопоточность.
                              0
                              То есть, PCA, рассчитанная на 16 серв, управляет каждым по очереди? Вот почему она 150 р. стоит!
                                0
                                Все же нет, посмотрел сейчас исходник github.com/adafruit/Adafruit_Python_PCA9685/blob/master/Adafruit_PCA9685/PCA9685.py, каналы назначаютс отдельно.

                                Думаю, когда юзер двигает сразу несколько слайдеров, генерится большое число запросов одновременно. Возможно, можно настроить многопоточность в Flask, или даже имело бы смысл использовать websocket, стало бы быстрее.
                                  0
                                  Самый простой вариант для пробы — добавить threaded=true как описано здесь: medium.com/@dkhd/handling-multiple-requests-on-flask-60208eacc154

                                  Тогда желательно добавить блокировку, чтобы несколько запросов к PCA9685_pwm.set_pwm не выполнялись одновременно, иначе в I2C будут неправильные данные, если все смешается.
                                    0

                                    Сама по себе PCA9685 умеет генерирует 16 ШИМов совершенно независимо друг от друга. Задать новые значения ШИМа одновременно, правда, не получится (и авторы микросхемы не заморачивались с тем, чтобы новые значения применялись одновременно), однако там есть возможность загрузить новые значения за один обмен по шине. С учётом скорости шины в 1 МГц (правда, насколько помню, RPi умеет только до 100 кГц) получается довольно бодро.


                                    Правда, господа из Adafruit этот режим не осилили, и пишут мало того, что в каждый канал по отдельности, так ещё и в каждый канал по 4 транзакции вместо одной...

                                      0
                                      Благодарю, попробую переварить.
                                        0

                                        Да что там переваривать-то...


                                        Вам надо добавить установку 5-го бита в регистр MODE1 (см. даташит, страница 14) куда-нибудь в PCA9685 __init__()
                                        и переделать


                                            def set_pwm(self, channel, on, off):
                                                """Sets a single PWM channel."""
                                                self._device.write8(LED0_ON_L+4*channel, on & 0xFF)
                                                self._device.write8(LED0_ON_H+4*channel, on >> 8)
                                                self._device.write8(LED0_OFF_L+4*channel, off & 0xFF)
                                                self._device.write8(LED0_OFF_H+4*channel, off >> 8)

                                        во что-то вроде


                                            def set_pwm(self, channel, on, off):
                                                """Sets a single PWM channel."""
                                                # onL, onH, offL, offH
                                                self._device.writeList(LED0_ON_L + 4*channel, [on&0xff, on>>8, off&0xff, off>>8])

                                        Это уже ускорит обмен в раза в 2-3. Для дальнейшего ускорения надо переделать архитектуру — собирать все параметры в одну кучу, и засылать единоразово по какому-то событию.
                                        Кстати, я был неправ — в этом случае все значения ШИМов будут обновляться одновременно, по окончанию передачи.


                                        И да, есть большая вероятность, что "затык" в чём-то другом. Но слово "Flask" мне не очень знакомо, здесь я не помогу...

                                          0
                                          Сам код до безобразия прост —
                                          код
                                          from flask import Flask
                                          from flask import request

                                          import time
                                          #import atexit

                                          # Importiere die Adafruit PCA9685 Bibliothek
                                          import Adafruit_PCA9685
                                          #from pca9685 import *

                                          # Initialise the PCA9685 using the default address (0x40).
                                          PCA9685_pwm = Adafruit_PCA9685.PCA9685()
                                          #servo = PCA9685()

                                          # Alternatively specify a different address and/or bus:
                                          #pwm = Adafruit_PCA9685.PCA9685(address=0x41, busnum=2)

                                          # Set frequency to 100hz, good for l298n h-bridge.
                                          PCA9685_pwm.set_pwm_freq(60)
                                          #PCA9685.setPWM(1,on)

                                          # Configure min and max servo pulse lengths
                                          servo_min = 150 # Min pulse length out of 4096
                                          servo_max = 600 # Max pulse length out of 4096

                                          app = Flask(__name__)

                                          app.route("/")
                                          def web_interface():
                                          html = open(«index.html»)
                                          response = html.read().replace('\n', '')
                                          html.close()
                                          return response

                                          app.route("/set_servo1")
                                          def set_servo1():
                                          speed = request.args.get(«speed»)
                                          print («Received » + str(speed))
                                          PCA9685_pwm.set_pwm(0, 0, int(speed))
                                          return «Received » + str(speed)

                                          # 2 servos (pca — 1,2 channels) contolled by «servo-2» slider on web-page
                                          app.route("/set_servo2")
                                          def set_servo2():
                                          speed = request.args.get(«speed»)
                                          a=tuple(speed)
                                          #print (int(a[2]))
                                          x=0
                                          if int(a[0])==1: #value servo 150-199
                                          if int(a[1])==5:
                                          x=(int(a[2]))*5
                                          elif int(a[1])==6:
                                          x=(int(a[2]))*5+50
                                          elif int(a[1])==7:
                                          x=(int(a[2]))*5+100
                                          elif int(a[1])==8:
                                          x=(int(a[2]))*5+150
                                          elif int(a[1])==9:
                                          x=(int(a[2]))*5+200

                                          elif int(a[0])==2: # value servo 200-299
                                          if int(a[1])==0:
                                          x=(int(a[2]))*5+250
                                          elif int(a[1])==1:
                                          x=(int(a[2]))*5+300
                                          elif int(a[1])==2:
                                          x=(int(a[2]))*5+350
                                          elif int(a[1])==3:
                                          x=(int(a[2]))*5+400
                                          elif int(a[1])==4:
                                          x=(int(a[2]))*5+450
                                          elif int(a[1])==5:
                                          x=(int(a[2]))*5+500
                                          elif int(a[1])==6:
                                          x=(int(a[2]))*5+550
                                          elif int(a[1])==7:
                                          x=(int(a[2]))*5+600
                                          elif int(a[1])==8:
                                          x=(int(a[2]))*5+650
                                          elif int(a[1])==9:
                                          x=(int(a[2]))*5+700

                                          elif int(a[0])==3: # value servo 300-399
                                          if int(a[1])==0:
                                          x=(int(a[2]))*5+750
                                          elif int(a[1])==1:
                                          x=(int(a[2]))*5+800
                                          elif int(a[1])==2:
                                          x=(int(a[2]))*5+850
                                          elif int(a[1])==3:
                                          x=(int(a[2]))*5+900
                                          elif int(a[1])==4:
                                          x=(int(a[2]))*5+950
                                          elif int(a[1])==5:
                                          x=(int(a[2]))*5+1000
                                          elif int(a[1])==6:
                                          x=(int(a[2]))*5+1050
                                          elif int(a[1])==7:
                                          x=(int(a[2]))*5+1100
                                          elif int(a[1])==8:
                                          x=(int(a[2]))*5+1150
                                          elif int(a[1])==9:
                                          x=(int(a[2]))*5+1200

                                          elif int(a[0])==4: # value servo 400-499
                                          if int(a[1])==0:
                                          x=(int(a[2]))*5+1250
                                          elif int(a[1])==1:
                                          x=(int(a[2]))*5+1300
                                          elif int(a[1])==2:
                                          x=(int(a[2]))*5+1350
                                          elif int(a[1])==3:
                                          x=(int(a[2]))*5+1400
                                          elif int(a[1])==4:
                                          x=(int(a[2]))*5+1450
                                          elif int(a[1])==5:
                                          x=(int(a[2]))*5+1500
                                          elif int(a[1])==6:
                                          x=(int(a[2]))*5+1550
                                          elif int(a[1])==7:
                                          x=(int(a[2]))*5+1600
                                          elif int(a[1])==8:
                                          x=(int(a[2]))*5+1650
                                          elif int(a[1])==9:
                                          x=(int(a[2]))*5+1700

                                          elif int(a[0])==5: # value servo 500-599
                                          if int(a[1])==0:
                                          x=(int(a[2]))*5+1750
                                          elif int(a[1])==1:
                                          x=(int(a[2]))*5+1800
                                          elif int(a[1])==2:
                                          x=(int(a[2]))*5+1850
                                          elif int(a[1])==3:
                                          x=(int(a[2]))*5+1900
                                          elif int(a[1])==4:
                                          x=(int(a[2]))*5+1950
                                          elif int(a[1])==5:
                                          x=(int(a[2]))*5+2000
                                          elif int(a[1])==6:
                                          x=(int(a[2]))*5+2050
                                          elif int(a[1])==7:
                                          x=(int(a[2]))*5+2100
                                          elif int(a[1])==8:
                                          x=(int(a[2]))*5+2150
                                          elif int(a[1])==9:
                                          x=(int(a[2]))*5+2200

                                          elif int(a[0])==6: # value servo 500-599
                                          if int(a[1])==0:
                                          x=(int(a[2]))*5+2250

                                          #print(x)
                                          speed2=(int(speed)*4)-x
                                          #speed2=servo_max-x

                                          PCA9685_pwm.set_pwm(1, 0, int(speed))
                                          PCA9685_pwm.set_pwm(2, 0, int(speed2))
                                          return «Received » + str(speed2)

                                          app.route("/set_servo3")
                                          def set_servo3():
                                          speed = request.args.get(«speed»)
                                          print («Received » + str(speed))
                                          PCA9685_pwm.set_pwm(3, 0, int(speed))
                                          return «Received » + str(speed)

                                          app.route("/set_servo4")
                                          def set_servo4():
                                          speed = request.args.get(«speed»)
                                          print («Received » + str(speed))
                                          PCA9685_pwm.set_pwm(4, 0, int(speed))
                                          return «Received » + str(speed)

                                          app.route("/set_servo5")
                                          def set_servo5():
                                          speed = request.args.get(«speed»)
                                          print («Received » + str(speed))
                                          PCA9685_pwm.set_pwm(5, 0, int(speed))
                                          return «Received » + str(speed)

                                          app.route("/set_servo6")
                                          def set_servo6():
                                          speed = request.args.get(«speed»)
                                          print («Received » + str(speed))
                                          PCA9685_pwm.set_pwm(6, 0, int(speed))
                                          return «Received » + str(speed)

                                          app.route("/set_servo7")
                                          def set_servo7():
                                          speed = request.args.get(«speed»)
                                          print («Received » + str(speed))
                                          PCA9685_pwm.set_pwm(7, 0, int(speed))
                                          return «Received » + str(speed)

                                          if __name__ == "__main__":
                                          app.run(host='0.0.0.0', port=8181, debug=True)


                                          Так понимаю, интересна строка:
                                          PCA9685_pwm.set_pwm_freq(60)

                                          Как интегрировать def set_pwm? Скормить все функции?

                                          Дело осложняется еще тем, что после динамичных передвижений слайдеров PCA задирает ток до 1,5 А и при этом проседает напряжение. И все стопорится, подвешивая raspberry.
                                          Может тестовый пост накатать на эту тему?

                                          *Поменял GET на POST, не сильно погоду изменило.

                                            0

                                            Осознать всю портянку кода не смог, извините. Особенно непонятны почти одинаковые ряды elif'ов. Вроде б оно в пару условий схлопывается...


                                            set_pwm_freq — это установка частоты ШИМа. Единое на все каналы (особенность PCA). Насколько я понимаю в сервах (а понимаю не очень много...), рекомендуемая частота для встроенного в серву драйвера — полсотни-сотня герц. Т.е. тут всё вроде б нормально.


                                            Как интегрировать def set_pwm?

                                            What do you mean? Я просто предлагал переписать адафруктовое безобразие.


                                            Дело осложняется еще тем, что после динамичных передвижений слайдеров PCA задирает ток до 1,5 А и при этом проседает напряжение.

                                            Сама PCA вряд-ли столько может потребить (там абсолютный максимум по паспорту — 0.4 ватта), это какой-то моторчик с ума сходит.
                                            Кажется, Дмитрий прав, и тут действительно идёт запись в несколько потоков.
                                            Попробуйте натыкать косты.. отладку:


                                                def set_pwm(self, channel, on, off):
                                                    if self.cur_channel is not None:
                                                        как_там_у_фласка_запись_в_лог ("Error! Simultanious write in two channels!!")
                                                    self.cur_channel = channel
                                                    # тут, собственно, пишем
                                                    self.cur_channel = None

                                            И в случае появления подобных сообщений думать, как их лечить (а лечить надо обязательно! какая каша будет на I2C в этом случае, даже разбираться лень).
                                            Хотя, если желание поразбираться есть, купите у китайцев клон Saleae Logic. Замечательная штуковина для отладки всяких подобных околожелезных вопросов. Стоит рублей 400 всего.


                                            PS DmitrySpb79, мы вам тут не сильно мешаем? Питон есть, распбери тоже, но от темы мы явно отклонились :-)

                                              0
                                              Мне не жалко :)

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

                                              Но просто разрешить многопоточность в Flask недостаточно, надо переписать код работы с PCA9685, чтобы библиотека корректно параллельные запросы обрабатывала.
                                                0
                                                разрешить многопоточность в Flask

                                                … как раз не нужно. Оно ж наверняка гораздо быстрее, чем обмен по I2C. Имхо.


                                                Наверное, где-то что-то портится, в регистры PCA пишется мусор, и какой-то моторчик сходит с ума (я не в курсе, что будет с сервой, если её кормить "нестандартным" ШИМом).


                                                Диагностика — для начала выводом передаваемых данных из адафрукт-библиотеки, потом логическим анализатором на входе и на выходе PCA.
                                                Опять же, имхо околожелезячного программиста.

                                                  0
                                                  Осознать всю портянку кода не смог, извините. Особенно непонятны почти одинаковые ряды elif'ов. Вроде б оно в пару условий схлопывается...

                                                  elifы — это костыль для серво, чтобы он крутился в обратную сторону параллельно с другим серво. Так как в «плече» два серво.
                                                  Отладка во flaske уже включена (в конце кода debug=True).
                                                  В том-то и фикус, сто в дебагер ничего не падает. С точки зрения программы все работает корректно. Так же корректно подвисает.
                                                  Здесь видео работы с нагрузкой, но и без нее проблемы возникают:
                                                  видео



                                                  я не в курсе, что будет с сервой, если её кормить «нестандартным» ШИМом

                                                  Просто не работает серва.

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

                      Самое читаемое