Bubot

Материал из razgovorov.ru
Перейти к: навигация, поиск

Bubot - фреймворк на Python 3 для программирования роботов и домашней автоматизации, предоставляющий функционал для распределенной работы.


Предыстория

Хотелось создать своего робота, а так же автоматизировать управление светом и климатом дома. С этой цель начал изучать имеющиеся возможности - рассматривал все варианты систем где бы робот состоял из параллельно работающих процессов обменивающихся между собой сообщениями. Из наиболее популярных подходили Microsoft Robotics Studio и ROS, и все бы ничего, но на текущий момент привязать их к конкретному железу весьма не просто, ну и самое главное писать на языке C очень не хотелось. Душа просила чего-нибудь по проще и по легче – как например Python. Учитывая, что нужно было и робота и умный дом, да ещё почти сразу появились перспективы другого применения, то было решено сделать небольшой фреймворк в котором упор делался на простоту разработки.

Концепция

Файл:Bubot.png
Bubot - Схема

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

Сеть процессов строится на базе стандартного Python модуля multiprocessing. Система сообщений и разделяемая память реализованы при помощи Redis (входящие в состав модуля multiprocessing очереди и разделяемая память не совсем подошли под мои задачи и сильно усложнили бы систему).

Каждый Bubot имеет встроенный веб-сервер Tornado, который позволяет контролировать состояние, управлять роботом, на ходу менять параметры (калибровать)робота, а также закладывается возможность обмена данными между роботами. Bubot не является системой реального времени, хотя Bubot возможно интегрировать с кодом реального времени.

Hello Bubot

Изучать что либо новое всегда проще на примере, и первое что приходит в голову это переделать радиоуправляемую игрушку на управление с помощью веб-интерфейса через wi-fi или 3G.

Нам понадобится любая китайская радиоуправляемая машинка и любой мини компьютер с установленным python 3 (я использовал raspberry pi b). Изначально практически любая радиоуправляемая машинка это два мотора и примитивный радио модуль. Нам от неё надо только два мотор и чтобы сама машинка была подходящего размера.

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

Для решения поставленной задачи нам необходимо:

  1. Подключить моторы к raspberry pi.
  2. Реализовать сервис который будет принимать и интерпретировать команды пользователя на конкретные физические устройства.
  3. Реализовать веб интерфейс, который будет передавать команды пользователя: Установить мощность основного или поворотного двигателя -100% / 0% / 100%.

Подключаем моторы

Файл:Bubot-easy-scout.png
Простейший пример - Схема подключения

Мне кажется простейшим способом подключения моторов к raspberry pi будет использование готового контроллера, выбор которого основывается на предполагаемой мощности моторов. Я выбрал с запасом на базе L298N. Строка для поиска на aliexpress " L298N motor driver board", обойдется Вам примерно в $3 с доставкой.


Также Вам понадобится как минимум один понижающий преобразователь напряжения для питания raspberry от того что будет на борту Вашей машинки, я взял на базе LM2596. Строка для поиска на aliexpress "DC-DC LM2596", обойдется Вам примерно в $1 с доставкой.


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

Реализуем модуль мотора

В целях упрощения модели, пусть у нас команды поступают непосредственно на моторы. Поскольку у нас два одинаковых (с программной точки зрения) мотора, то нам потребуется один модуль. Модули в фреймворке находятся в каталоге buject. Каждый модуль состоит из двух файлов:

  • [название модуля].py - содержит логику модуля, все модули наследуются либо от базового класса Buject, либо от его потомков. От базового класса разработчик получает основной бесконечный цикл, методы для обработки событий.
  • [название модуля].json - содержит описание модуля - список параметров модуля, их значения по умолчанию, список возможных статусов, описание сообщений генерируемых модулем, и список сообщений на которые модуль подписан.

Пример модуля для наших моторов \buject\Motor.py

import json
from buject.Buject import Buject
import RPi.GPIO as GPIO

class Motor(Buject):  # сервомотор без обратной связи
    def __init__(self, user_config=None):
        super(Motor, self).__init__(user_config)

    def on_ready(self):    # выставляем режим работы GPIO
        GPIO.setmode(self.param["mode"])
        GPIO.setwarnings(False)

    def incoming_request_set_power(self, message): # методу на входящий запрос set_power в качестве параметра передается все сообщение
        data = json.loads(message['data'])  
        
        if data['param']['value'] > 0:  # хотим ехать вперед
            GPIO.setup(self.param['GPIO_reward'], GPIO.OUT, 0)
            GPIO.setup(self.param['GPIO_forward'], GPIO.OUT, 1)
            self.status['action'] = "forward"
        elif data['param']['value'] == 0:
            GPIO.setup(self.param['GPIO_forward'], GPIO.OUT, 0)
            GPIO.setup(self.param['GPIO_reward'], GPIO.OUT, 0)
            self.status['action'] = "stopped"
        else:
            GPIO.setup(self.param['GPIO_forward'], GPIO.OUT, 0)
            GPIO.setup(self.param['GPIO_reward'], GPIO.OUT, 1)
            self.status['action'] = "backward {0}%".format(self.status["power"])

        if self.param['debug'] > 1:  # в режиме отладки получаем сообщение что все отработало
            self.log('Buject "{0}" {1}'.format(self.param['name'], self.status['action']))

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

Бесконечный цикл в данном случае задействован не был. В случае его наличия достаточно определить метод main_loop().

Пример описания модуля для наших моторов \buject\Motor.json

{
    "param": {
        "name": {
            "value": "Motor",
            "description": "название сервиса по умолчанию"
        },
        "parent": {
            "value": "Buject",
            "description": "название базового модуля, с которого наследуются другие параметры"
        },
        "buject": {
            "value": "Motor",
            "description": "название модуля"
        },
        "GPIO_forward": {
            "value": 0,
            "description": "канал GPIO для движения вперед"
        },
        "GPIO_backward": {
            "value": 0,
            "description": "канал GPIO для движения назад"
        },
        "GPIO_mode": {
            "value": 11,
            "description": "value for GPIO.setmode GPIO.BOARD=10 GPIO.BCM=11"
        }
    },
    "incoming_request": {
        "set_power": {
            "name": "set_power",
            "description": "установка мощности мотора",
            "param": {
                "value": {
                    "description": "мощность мотора в процентах",
                    "type": "int"
}   }   }   }   }

Раздел param содержит список параметров необходимых для запуска и работы модуля. Первые три обязательные для каждого модуля, и наследуются от базового класса Buject. Последние являются специфичными только для этого, их количество и название Вы придумываете сами в зависимости от потребностей. GPIO_mode задает режим адресации GPIO и в дальнейшем переопределяться не будет. В то время как GPIO_forward и GPIO_backward нет смысла определять, т.к. зависят исключительно от того к каким выводам будет подключен будущий мотор и мы их определим дальше в параметрах запуска этого модуля.

Также описание модуля может содержать секцию status - где описаны все рассчитываемые параметры отражающие текущее состояние модуля. В данном случае специально для модуля Motor нет никаких добавленных статусов, однако, если Вы обратили внимание в коде самого модуля мы выставляем один из статусов 'action' который был определен в описании базового класса Buject.

Фреймворк предоставляет возможность использовать пять типов сообщений:

  • incoming_request - входящие запросы, декларируется список запросов которые может обрабатывать модуль.
  • outgoing_request - исходящие запросы, в качестве параметров обязательно указать имя сервиса принимающего запросы и имя запроса.
  • incoming_event - входящие события, список подписки на события других модулей, обязательно указать имя сервиса и имя события.
  • outgoing_event - исходящие события, декларируется список событий на которые могут подписаться другие модули.
  • incoming_response - служебный тип, который декларируется на исходящем запросе, говорит о том, что сервис будет ожидать асинхронного ответа на запрос.

Реализуем веб интерфейс

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

Пользовательские Веб интерфейсы хранятся в каталоге ui. Каждая страница пользовательского интерфейса описывается в отдельном подкаталоге, и состоит как минимум из 2 файлов:

  • [Имя страницы].html - разметка страницы.
  • [Имя страницы].json - каждая страница (сессия) для фреймворка является по сути отдельным сервисом, в данном файле содержится описание событий на которые данная страница подписана, а также сообщения которые она генерирует.
  • [Имя страницы].py - не обязательно, может содержать серверную логику по обработке команд пользовательского интерфейса, в нашем случае не пригодится.

Давайте опять для улучшения восприятия ещё немного упростим. В приведенном ниже примере алгоритм одной кнопки вперед. Остальные можно сделать по аналогии. Итак создаем в каталоге ui подкаталог scout_easy и в нем два файла scout_easy.html и scout_easy.json следующего содержания (комментарии по коду).

\ui\scout_easy\scout_easy.html

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/static/js/jquery-ui-1.11.2/jquery-ui.css">
    <script type="text/javascript" src="/static/js/jquery-2.1.3.min.js"></script>
    <script type="text/javascript" src="/static/js/jquery-ui-1.11.2/jquery-ui.js"></script>
    <script type="text/javascript" src="/ui/bubot_socket.js"></script>
    <title>BuBot</title>
    <script>
        function bubot_on_open() {
        }
        function get_bubot_actions() {
            return {};
        }
        $(function () {
            $("#command_move_forward").button({}).mousedown(function () {
                bubot_send_message('send_request', {'name': "set_drive_motor_power", 'data': {'value': 100}});
            }).mouseup(function () {
                bubot_send_message('send_request', {'name': "set_drive_motor_power", 'data': {'value': 0}});
            });
        })
    </script>
</head>
<body class="ui-widget-content">
<button id="command_move_forward" class="command_button"></button>
<div id="console" class="ui-widget-content"></div>
</body>
</html>

Обратите внимание на div id=console, если он присутствует, то в него фреймворк будет выводить все консольные сообщения, в т.ч. происходящие на стороне сервера ошибки кода.

bubot_socket.js - должен присутствовать на каждой странице ui, так как именно он отвечает за установку соединения и обмен сообщениями.

соединение с сервером происходит через web socket, метод bubot_send_message([название сообщения], [параметры сообщения]) отвечает за передачу сообщений на сервер. На сервере при поступлении сообщения вызывается одноименный метод, которому передаются параметры сообщения. В нашем случае вызывается метод отправляющий запрос set_drive_motor_power, имя сервиса получателя сообщения фреймворк берет из файла описания пользовательского интерфейса.

\ui\scout_easy\scout_easy.json

{
    "incoming_request": {
        "console": {
            "time": {},
            "message": {}
        }
    },
    "outgoing_request": {
        "set_drive_motor_power": {
            "name": "set_power",
            "buject": "Motor",
            "description": "команда на установку мощности основного мотора",
            "param": {
                "value": {
                    "description": "мощность мотора в процентах, вперед > 0, назад < 0",
                    "type": "int"
                }
            }
        },
        "set_rotation_motor_power": {
            "name": "set_power",
            "buject": "Motor",
            "description": "команда на установку мощности рулевого мотора",
            "param": {
                "value": {
                    "description": "мощность мотора в процентах, вправо > 0, влево < 0",
                    "type": "int"
}   }   }   }   }

Запускаем робота

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

В каталоге config хранятся конфиги всех Ваших роботов. Например имеет смысл делать конфиг из одного модуля для его отладки.

\config\scout_easy.json

{
    "param": {
        "name": {
            "value": "scout_easy"
        }
    },
    "depend_buject": {   # раздел содержит список сервисов из которых состоит робот
        "drive_motor": { # название сервиса, ниже присваиваем значения только тем параметрам, которые отличаются от значений по умолчанию в соответствующем модуле
            "param": {
                "buject": {  # название модуля из которого будет запущен сервис
                    "value": "Motor"
                },
                "name": {    # название сервиса
                    "value": "drive_motor"
                },
                "GPIO_forward": {  # назначаем каналы к которым фактически подключен мотор
                    "value": 20
                },
                "GPIO_reward": {
                    "value": 21
                }
            }
        },
        "rotation_motor": {
            "param": {
                "buject": {
                    "value": "Motor"
                },
                "name": {
                    "value": "rotation_motor"
                },
                "GPIO_forward": {
                    "value": 13
                },
                "GPIO_reward": {
                    "value": 19
}   }   }   }   }

В конфиге мы описали что надо запустить два экземпляра модуля Motor с разными параметрами. Как вы видите способ адресации GPIO мы не указали, он у нас унаследуется от модуля, а вот параметры GPIO_forward и GPIO_backward мы переопределили в соответствии со схемой подключения.

Теперь у нас совсем все готово. Можно запускать.

python3 StartBubot scout_easy

Теперь можно открыть свой пользовательский интерфейс в браузере http://localhost/ui/scout_easy и попробовать. При первом запуске Вас попросят ввести логин и пароль - введите любые значения, по умолчанию права доступа к системе не установлены.

Конфигуратор доступен по адресу http://localhost/ui/studio

Bubot scout

Файл:Scout.png
Bubot:scout - Схема подключения
Bubot:scout - Схема сервисов

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


Полный пример кода bubot scout входит в состав дистрибутива Bubot. Схематично сервисы там выглядят следующим образом.


Предыдущий пример я специально сделал упрощенным. Реализуя более сложные алгоритмы имеет смысл определить интерфейсы для типовых устройств - Мотор, Серва. А уже их реализацию под конкретное железо - драйвера делать через наследование. В качестве примера можно посмотреть классы PCA9685Motor который по сути стал драйвером для моторов подключаемых через генератор шим - PCA9685. Также в отдельные модули я вынес работу i2c и GPIO чтобы не было проблем с блокировками и т.п.

Более подробно где скачать, как установить фреймворк можно почитать тут. На очереди пример реализации на базе фреймворка контроллера умного дома.