Bubot — различия между версиями

Материал из razgovorov.ru
Перейти к: навигация, поиск
(Запускаем робота)
(Запускаем робота)
 
(не показана 21 промежуточная версия этого же участника)
Строка 1: Строка 1:
Bubot - фреймворк на Python 3 для программирования роботов и домашней автоматизации, предоставляющий функционал для распределенной работы.  
+
Bubot - очень легкий фреймворк на Python 3 для программирования роботов и домашней автоматизации, предоставляющий функционал для распределенной работы.
  
  
 
==Предыстория==
 
==Предыстория==
Хотелось создать своего робота, а так же автоматизировать управление светом и климатом дома. С этой цель начал изучать имеющиеся возможности - рассматривал все варианты систем где бы робот состоял из параллельно работающих процессов обменивающихся между собой сообщениями. Из наиболее популярных подходили [https://ru.wikipedia.org/wiki/Microsoft_Robotics_Developer_Studio Microsoft Robotics Studio] и [http://robocraft.ru/blog/robosoft/721.html ROS], и все бы ничего, но на текущий момент привязать их к конкретному железу весьма не просто, ну и самое главное писать на языке C очень не хотелось. Душа просила чего-нибудь по проще и по легче – как например Python. Учитывая, что нужно было и робота и умный дом, да ещё почти сразу появились перспективы другого применения, то было решено сделать небольшой фреймворк в котором упор делался на простоту разработки.
+
Хотелось создать своего робота, а так же автоматизировать управление светом и климатом дома. С этой цель начал изучать имеющиеся возможности - рассматривал все варианты систем где бы робот состоял из параллельно работающих процессов обменивающихся между собой сообщениями.
 +
 
 +
Из наиболее популярных подходили [https://ru.wikipedia.org/wiki/Microsoft_Robotics_Developer_Studio Microsoft Robotics Studio] и [http://robocraft.ru/blog/robosoft/721.html ROS], и все бы ничего, но на текущий момент привязать их к конкретному железу весьма не просто, разобраться в них с нуля так же не тривиальная задача, ну и самое главное писать на языке C очень не хотелось. Душа просила чего-нибудь по проще и по легче.
 +
 
 +
Учитывая, что нужно было и робота и умный дом, да ещё почти сразу появились перспективы другого применения, то было решено сделать небольшой фреймворк в котором упор делался на простоту разработки.
  
 
==Концепция==
 
==Концепция==
  
[[Файл:Bubot.png|400px|thumb|right|Bubot - Схема]]
+
Концептуально фреймворк представляет собой набор поддерживаемых пользователем модулей реализующих различные функции робототехники. При работе Bubot строит сеть из процессов, которые могут асинхронно получать и отправлять сообщения между собой. Так же Вы можете построить сеть из роботов, которые будут общаться между собой.
Концептуально фреймворк представляет собой набор поддерживаемых пользователей модулей реализующих различные функции робототехники. При работе Bubot строит сеть из процессов, которые могут асинхронно получать и отправлять сообщения между собой.  
+
 
 +
[[Файл:Bubot_scheme.png|700px]]
  
Сеть процессов строится на базе стандартного Python модуля multiprocessing. Система сообщений и разделяемая память реализованы при помощи [http://devacademy.ru/posts/vvedenie-v-redis-py/ Redis] (входящие в состав модуля multiprocessing  очереди и разделяемая память не совсем подошли под мои задачи и сильно усложнили бы систему).
+
Сеть процессов строится на базе стандартного Python модуля multiprocessing. Система сообщений и разделяемая память реализованы при помощи [http://devacademy.ru/posts/vvedenie-v-redis-py/ Redis].
  
Каждый Bubot имеет встроенный [http://wiki.python.su/%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D0%B8/Tornado-web?highlight=%28%28%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D0%B8%7CTornado-web%29%29 веб-сервер Tornado], который позволяет контролировать состояние, управлять роботом, на ходу менять параметры (калибровать)робота, а также закладывается возможность обмена данными между роботами.
+
Каждый Bubot имеет встроенный веб-сервер [http://wiki.python.su/%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D0%B8/Tornado-web?highlight=%28%28%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D0%B8%7CTornado-web%29%29 веб-сервер Tornado], который позволяет контролировать состояние, управлять роботом, на ходу менять параметры (калибровать)робота, а также закладывается возможность обмена данными между роботами.
 
Bubot не является системой реального времени, хотя Bubot возможно интегрировать с кодом реального времени.
 
Bubot не является системой реального времени, хотя Bubot возможно интегрировать с кодом реального времени.
  
 
==Hello Bubot==
 
==Hello Bubot==
 +
 
Изучать что либо новое всегда проще на примере, и первое что приходит в голову это переделать радиоуправляемую игрушку на управление с помощью веб-интерфейса через wi-fi или 3G.
 
Изучать что либо новое всегда проще на примере, и первое что приходит в голову это переделать радиоуправляемую игрушку на управление с помощью веб-интерфейса через wi-fi или 3G.
  
Нам понадобится любая китайская радиоуправляемая машинка и любой мини компьютер с установленным python 3 (я использовал raspberry pi b).
+
Нам понадобится любая китайская радиоуправляемая машинка и любой мини компьютер с установленным python 3 (я использовал один из самых доступных - raspberry pi b+).
Изначально практически любая радиоуправляемая машинка это два мотора и примитивный радио модуль. Нам от неё надо только два мотор  и чтобы сама машинка была подходящего размера.
+
Изначально практически любая радиоуправляемая машинка - это два мотора и примитивный радио модуль. Нам от неё надо только моторы и чтобы сама машинка была подходящего размера чтобы поместить на неё всю электронику.
  
Для начала упростим задачу - наша машинка должна выполнять четыре действия: ехать вперед или назад, поворачивать влево или вправо.  
+
Для начала упростим задачу - наша машинка должна выполнять четыре действия: ехать вперед или назад, поворачивать влево или вправо.
  
 
Для решения поставленной задачи нам необходимо:
 
Для решения поставленной задачи нам необходимо:
# Подключить моторы к raspberry pi.
+
 
# Реализовать сервис который будет принимать и интерпретировать команды пользователя на конкретные физические устройства.
+
* Подключить моторы к raspberry pi
# Реализовать веб интерфейс, который будет передавать команды пользователя: Установить мощность основного или поворотного двигателя -100% / 0% / 100%.
+
* Реализовать сервис который будет принимать и интерпретировать команды пользователя на конкретные физические устройства
 +
* Реализовать веб интерфейс, который будет передавать команды пользователя: Установить мощность основного или поворотного двигателя -100% / 0% / 100%.
  
 
===Подключаем моторы===  
 
===Подключаем моторы===  
[[Файл:bubot-easy-scout.png|300px|thumb|right|Простейший пример - Схема подключения]]
 
Мне кажется простейшим способом подключения моторов к raspberry pi будет использование готового контроллера, выбор которого основывается на предполагаемой мощности моторов. Я выбрал с запасом на базе L298N. Строка для поиска на aliexpress " L298N motor driver board", обойдется Вам примерно в $3 с доставкой.
 
 
  
Также Вам понадобится как минимум один понижающий преобразователь напряжения для питания raspberry от того что будет на борту Вашей машинки, я взял на базе LM2596. Строка для поиска на aliexpress "DC-DC LM2596", обойдется Вам примерно в $1 с доставкой.  
+
Мне кажется простейшим способом подключения моторов к raspberry pi будет использование готового контроллера, выбор которого основывается зависит только от предполагаемой мощности моторов. Я выбрал с запасом на базе L298N. Строка для поиска на aliexpress " L298N motor driver board", обойдется Вам примерно в $3 с доставкой.  
  
 +
[[Файл:scout_easy_scheme.png|500px]]
  
 +
Также Вам понадобится как минимум один понижающий преобразователь напряжения для питания raspberry от того, что будет на борту Вашей машинки. Я взял на базе LM2596. Строка для поиска на aliexpress "DC-DC LM2596", обойдется Вам примерно в $1 с доставкой.
  
 
При таком подключении, чтобы заставить машину выполнить одну из наших команд достаточно выставить высокий уровень на соответствующем GPIO.
 
При таком подключении, чтобы заставить машину выполнить одну из наших команд достаточно выставить высокий уровень на соответствующем GPIO.
Строка 42: Строка 48:
 
В целях упрощения модели, пусть у нас команды поступают непосредственно на моторы.
 
В целях упрощения модели, пусть у нас команды поступают непосредственно на моторы.
 
Поскольку у нас два одинаковых (с программной точки зрения) мотора, то нам потребуется один модуль. Модули в фреймворке находятся в каталоге buject. Каждый модуль состоит из двух файлов:
 
Поскольку у нас два одинаковых (с программной точки зрения) мотора, то нам потребуется один модуль. Модули в фреймворке находятся в каталоге buject. Каждый модуль состоит из двух файлов:
* [название модуля].py - содержит логику модуля, все модули наследуются либо от базового класса Buject, либо от его потомков. От базового класса разработчик получает основной бесконечный цикл, методы для обработки событий.
+
 
 +
* [название модуля].py - содержит логику модуля, все модули наследуются либо от базового класса Buject, либо от его потомков. От базового класса разработчик получает основной бесконечный цикл, методы для обработки и передачи событий
 
* [название модуля].json - содержит описание модуля - список параметров модуля, их значения по умолчанию, список возможных статусов, описание сообщений генерируемых модулем, и список сообщений на которые модуль подписан.
 
* [название модуля].json - содержит описание модуля - список параметров модуля, их значения по умолчанию, список возможных статусов, описание сообщений генерируемых модулем, и список сообщений на которые модуль подписан.
  
Пример модуля для наших моторов \buject\Motor.py
+
Пример модуля для наших моторов \buject\MotorTest.py
  
 
<source lang=python>
 
<source lang=python>
Строка 52: Строка 59:
 
import RPi.GPIO as GPIO
 
import RPi.GPIO as GPIO
  
class Motor(Buject):  # сервомотор без обратной связи
+
class MotorTest(Buject):  # сервомотор без обратной связи
 
     def __init__(self, user_config=None):
 
     def __init__(self, user_config=None):
         super(Motor, self).__init__(user_config)
+
         super(MotorTest, self).__init__(user_config)
  
     def on_ready(self):    # выставляем режим работы GPIO
+
    # Метод вызывается перед запуском основго цикла
 +
    # выставляем режим работы GPIO
 +
     def on_ready(self):     
 
         GPIO.setmode(self.param["mode"])
 
         GPIO.setmode(self.param["mode"])
 
         GPIO.setwarnings(False)
 
         GPIO.setwarnings(False)
  
     def incoming_request_set_power(self, message): # методу на входящий запрос set_power в качестве параметра передается все сообщение
+
    # фреймворк при получения сообщения, вызывает одноименный метод для его обработки
         data = json.loads(message['data'])
+
    # передавая ему в качестве параметра все сообщение
       
+
    # данный модуль у нас будет уметь принимать только один запрос
 +
    # set_power на установку мощности мотора в процентах   
 +
     def incoming_request_set_power(self, message):  
 +
         data = json.loads(message['data'])
 +
 
 
         if data['param']['value'] > 0:  # хотим ехать вперед
 
         if data['param']['value'] > 0:  # хотим ехать вперед
 
             GPIO.setup(self.param['GPIO_reward'], GPIO.OUT, 0)
 
             GPIO.setup(self.param['GPIO_reward'], GPIO.OUT, 0)
Строка 68: Строка 81:
 
             self.status['action'] = "forward"
 
             self.status['action'] = "forward"
 
         elif data['param']['value'] == 0:
 
         elif data['param']['value'] == 0:
             GPIO.setup(self.param['GPIO_forward'], GPIO.OUT, 0)
+
             GPIO.setup(self.param['GPIO_forward'], GPIO.OUT, 0)  
 
             GPIO.setup(self.param['GPIO_reward'], GPIO.OUT, 0)
 
             GPIO.setup(self.param['GPIO_reward'], GPIO.OUT, 0)
 
             self.status['action'] = "stopped"
 
             self.status['action'] = "stopped"
Строка 76: Строка 89:
 
             self.status['action'] = "backward {0}%".format(self.status["power"])
 
             self.status['action'] = "backward {0}%".format(self.status["power"])
  
         if self.param['debug'] > 1:  # в режиме отладки получаем сообщение что все отработало
+
        # в режиме отладки получаем сообщение, что все отработало
 +
         if self.param['debug'] > 1:   
 
             self.log('Buject "{0}" {1}'.format(self.param['name'], self.status['action']))
 
             self.log('Buject "{0}" {1}'.format(self.param['name'], self.status['action']))
 
</source>
 
</source>
Строка 85: Строка 99:
  
 
Пример описания модуля для наших моторов \buject\Motor.json
 
Пример описания модуля для наших моторов \buject\Motor.json
<source lang=text>
+
<source lang=javascript>
 
{
 
{
 
     "param": {
 
     "param": {
 
         "name": {
 
         "name": {
             "value": "Motor",
+
             "value": "MotorTest",
 
             "description": "название сервиса по умолчанию"
 
             "description": "название сервиса по умолчанию"
 
         },
 
         },
Строка 97: Строка 111:
 
         },
 
         },
 
         "buject": {
 
         "buject": {
             "value": "Motor",
+
             "value": "MotorTest",
             "description": "название модуля"
+
             "description": "название модуля = имени файла"
 
         },
 
         },
 
         "GPIO_forward": {
 
         "GPIO_forward": {
Строка 123: Строка 137:
 
}  }  }  }  }
 
}  }  }  }  }
 
</source>
 
</source>
Раздел param содержит список параметров необходимых для запуска и работы модуля. Первые три обязательные для каждого модуля, и наследуются от базового класса Buject. Последние являются специфичными только для этого, их количество и название Вы придумываете сами в зависимости от потребностей. GPIO_mode задает режим адресации GPIO и в дальнейшем переопределяться не будет. В то время как GPIO_forward и GPIO_backward нет смысла определять, т.к. зависят исключительно от того к каким выводам будет подключен будущий мотор и мы их определим дальше в параметрах запуска этого модуля.
+
Раздел param содержит список параметров необходимых для запуска и работы модуля. Первые три обязательные для каждого модуля, и наследуются от базового класса Buject. Последние являются специфичными только для этого модуля, их количество и название Вы придумываете сами в зависимости от потребностей. GPIO_mode задает режим адресации GPIO и в дальнейшем переопределяться не будет. В то время как для GPIO_forward и GPIO_backward нет смысла задавать значения по умолчанию, т.к. они зависят исключительно от того к каким выводам будет подключен конкретный мотор и мы их определим дальше в параметрах запуска этого модуля.
  
Также описание модуля может содержать секцию status - где описаны все рассчитываемые параметры отражающие текущее состояние модуля. В данном случае специально для модуля Motor нет никаких добавленных статусов, однако, если Вы обратили внимание в коде самого модуля мы выставляем один из статусов 'action' который был определен в описании базового класса Buject.
+
Также описание модуля может содержать секцию status - где описаны все рассчитываемые параметры отражающие текущее состояние модуля. В данном случае специально для модуля Motor нет никаких добавленных статусов, однако, если Вы обратили внимание в коде самого модуля мы выставляем один из статусов 'action' который определен в описании базового модуля Buject.
  
 
Фреймворк предоставляет возможность использовать пять типов сообщений:
 
Фреймворк предоставляет возможность использовать пять типов сообщений:
 +
 
* incoming_request - входящие запросы, декларируется список запросов которые может обрабатывать модуль.
 
* incoming_request - входящие запросы, декларируется список запросов которые может обрабатывать модуль.
 
* outgoing_request - исходящие запросы, в качестве параметров обязательно указать имя сервиса принимающего запросы и имя запроса.
 
* outgoing_request - исходящие запросы, в качестве параметров обязательно указать имя сервиса принимающего запросы и имя запроса.
Строка 138: Строка 153:
  
 
Пользовательские Веб интерфейсы хранятся в каталоге ui. Каждая страница пользовательского интерфейса описывается в отдельном подкаталоге, и состоит как минимум из 2 файлов:
 
Пользовательские Веб интерфейсы хранятся в каталоге ui. Каждая страница пользовательского интерфейса описывается в отдельном подкаталоге, и состоит как минимум из 2 файлов:
 +
 
* [Имя страницы].html - разметка страницы.
 
* [Имя страницы].html - разметка страницы.
 
* [Имя страницы].json - каждая страница (сессия) для фреймворка является по сути отдельным сервисом, в данном файле содержится описание событий на которые данная страница подписана, а также сообщения которые она генерирует.
 
* [Имя страницы].json - каждая страница (сессия) для фреймворка является по сути отдельным сервисом, в данном файле содержится описание событий на которые данная страница подписана, а также сообщения которые она генерирует.
* [Имя страницы].py - не обязательно, может содержать серверную логику по обработке команд пользовательского интерфейса, в нашем случае не пригодится.
+
* [Имя страницы].py - не обязателен, может содержать серверную логику по обработке команд пользовательского интерфейса, в нашем случае не пригодится.
  
Давайте опять для улучшения восприятия ещё немного упростим. В приведенном ниже примере алгоритм одной кнопки вперед. Остальные можно сделать по аналогии.
+
Давайте опять для улучшения восприятия ещё немного упростим. В приведенном ниже примере рассмотрим алгоритм одной кнопки вперед. Остальные можно сделать по аналогии.
Итак создаем в каталоге ui подкаталог scout_easy и в нем два файла scout_easy.html и scout_easy.json следующего содержания (комментарии по коду).
+
Итак создаем в каталоге ui подкаталог scout_easy и в нем два файла scout_easy.html и scout_easy.json следующего содержания (комментарии по тексту).
  
 
\ui\scout_easy\scout_easy.html
 
\ui\scout_easy\scout_easy.html
Строка 151: Строка 167:
 
<head lang="en">
 
<head lang="en">
 
     <meta charset="UTF-8">
 
     <meta charset="UTF-8">
     <link rel="stylesheet" href="/static/js/jquery-ui-1.11.2/jquery-ui.css">
+
     <link rel="stylesheet" href="/static/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/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="/static/jquery-ui-1.11.2/jquery-ui.js"></script>
     <script type="text/javascript" src="/ui/bubot_socket.js"></script>
+
     <script type="text/javascript" src="/static/bubot_socket.js"></script>
 
     <title>BuBot</title>
 
     <title>BuBot</title>
 
     <script>
 
     <script>
 
         function bubot_on_open() {
 
         function bubot_on_open() {
 +
            <!--функция должна быть определена на каждой странице,
 +
            вызывается после открытия WebSocketa для того чтобы запросить у сервера
 +
            начальные данные, пример использования можно посмотреть в ui/studio или ui/scout-->
 
         }
 
         }
 +
 
         function get_bubot_actions() {
 
         function get_bubot_actions() {
 +
            <!--функция должна быть определена на каждой странице, вызывается при загрузке страницы,
 +
            содержит объект из функций для обработки входящих сообщений.
 +
            Пример использования можно посмотреть в ui/studio или ui/scout-->
 
             return {};
 
             return {};
 
         }
 
         }
 +
 
         $(function () {
 
         $(function () {
 +
            <!-- вешаем обработчики на нашу кнопку-->
 
             $("#command_move_forward").button({}).mousedown(function () {
 
             $("#command_move_forward").button({}).mousedown(function () {
                 bubot_send_message('send_request', {'name': "set_drive_motor_power", 'data': {'value': 100}});
+
 
 +
                <!--описание функции ниже по тексту статьи,
 +
                    при нажатии устанавливаем мощность мотора 100%
 +
                    при отжатии устанавливаем мощность мотора в 0%-->
 +
                 bubot_send_message('send_request', {'name': "set_move_motor_power",
 +
                                                    'data': {'value': 100}});
 
             }).mouseup(function () {
 
             }).mouseup(function () {
                 bubot_send_message('send_request', {'name': "set_drive_motor_power", 'data': {'value': 0}});
+
                 bubot_send_message('send_request', {'name': "set_move_motor_power",
 +
                                                    'data': {'value': 0}});
 
             });
 
             });
 
         })
 
         })
Строка 172: Строка 203:
 
</head>
 
</head>
 
<body class="ui-widget-content">
 
<body class="ui-widget-content">
<button id="command_move_forward" class="command_button"></button>
+
    <button id="command_move_forward" class="command_button">forward</button>
<div id="console" class="ui-widget-content"></div>
+
    <div id="console" class="ui-widget-content"></div>
 
</body>
 
</body>
 
</html>
 
</html>
Строка 182: Строка 213:
 
bubot_socket.js - должен присутствовать на каждой странице ui, так как именно он отвечает за установку соединения и обмен сообщениями.
 
bubot_socket.js - должен присутствовать на каждой странице ui, так как именно он отвечает за установку соединения и обмен сообщениями.
  
соединение с сервером происходит через web socket, метод bubot_send_message([название сообщения], [параметры сообщения]) отвечает за передачу сообщений на сервер. На сервере при поступлении сообщения вызывается одноименный метод, которому передаются параметры сообщения. В нашем случае вызывается метод отправляющий запрос set_drive_motor_power, имя сервиса получателя сообщения фреймворк берет из файла описания пользовательского интерфейса.
+
Соединение с сервером происходит через web socket, метод bubot_send_message([название сообщения], [параметры сообщения]) отвечает за передачу сообщений на сервер. На сервере при поступлении сообщения вызывается одноименный метод, которому передаются параметры сообщения. В нашем случае вызывается метод отправляющий запрос set_move_motor_power, имя сервиса получателя сообщения фреймворк берет из файла описания пользовательского интерфейса, названия этих сервисов определяется в заключительном разделе при описании робота.
  
 
\ui\scout_easy\scout_easy.json
 
\ui\scout_easy\scout_easy.json
<source lang=text>
+
<source lang=javascript>
 
{
 
{
 
     "incoming_request": {
 
     "incoming_request": {
Строка 194: Строка 225:
 
     },
 
     },
 
     "outgoing_request": {
 
     "outgoing_request": {
         "set_drive_motor_power": {
+
         "set_move_motor_power": {
 
             "name": "set_power",
 
             "name": "set_power",
             "buject": "Motor",
+
             "buject": "move_motor",
 
             "description": "команда на установку мощности основного мотора",
 
             "description": "команда на установку мощности основного мотора",
 
             "param": {
 
             "param": {
Строка 205: Строка 236:
 
             }
 
             }
 
         },
 
         },
         "set_rotation_motor_power": {
+
         "set_rotate_motor_power": {
 
             "name": "set_power",
 
             "name": "set_power",
             "buject": "Motor",
+
             "buject": "rotate_motor",
 
             "description": "команда на установку мощности рулевого мотора",
 
             "description": "команда на установку мощности рулевого мотора",
 
             "param": {
 
             "param": {
Строка 219: Строка 250:
 
Итак мы подготовили все части робота, чтобы его запустить нужен ещё один файл, в котом мы опишем все его составные части.
 
Итак мы подготовили все части робота, чтобы его запустить нужен ещё один файл, в котом мы опишем все его составные части.
  
В каталоге config хранятся конфиги всех Ваших роботов. Например имеет смысл делать конфиг из одного модуля для его отладки.
+
В каталоге config хранятся конфиги всех Ваших роботов. Создавать конфиги можно как вручную, так и про помощи конфигуратора - но об этом подробнее на видео о bubot: scout.
 +
Итак, для нашего первого робота конфиг будет выглядеть следующим образом:
  
 
\config\scout_easy.json
 
\config\scout_easy.json
<source lang=text>
+
<source lang=javascript>
 
{
 
{
 
     "param": {
 
     "param": {
Строка 230: Строка 262:
 
     },
 
     },
 
     "depend_buject": {  # раздел содержит список сервисов из которых состоит робот
 
     "depend_buject": {  # раздел содержит список сервисов из которых состоит робот
         "drive_motor": { # название сервиса, ниже присваиваем значения только тем параметрам, которые отличаются от значений по умолчанию в соответствующем модуле
+
         "move_motor": { # название сервиса, ниже присваиваем значения только тем параметрам, которые отличаются от значений по умолчанию в соответствующем модуле
 
             "param": {
 
             "param": {
                 "buject": {  # название модуля из которого будет запущен сервис
+
                 "buject": {  # название модуля из которого будет запущен сервис  
                     "value": "Motor"
+
                     "value": "MotorTest"
 
                 },
 
                 },
 
                 "name": {    # название сервиса
 
                 "name": {    # название сервиса
                     "value": "drive_motor"
+
                     "value": "move_motor"
 
                 },
 
                 },
 
                 "GPIO_forward": {  # назначаем каналы к которым фактически подключен мотор
 
                 "GPIO_forward": {  # назначаем каналы к которым фактически подключен мотор
Строка 246: Строка 278:
 
             }
 
             }
 
         },
 
         },
         "rotation_motor": {
+
         "rotate_motor": { # аналогично для рулевого мотора
 
             "param": {
 
             "param": {
 
                 "buject": {
 
                 "buject": {
                     "value": "Motor"
+
                     "value": "MotorTest"
 
                 },
 
                 },
 
                 "name": {
 
                 "name": {
                     "value": "rotation_motor"
+
                     "value": "rotate_motor"
 
                 },
 
                 },
 
                 "GPIO_forward": {
 
                 "GPIO_forward": {
Строка 261: Строка 293:
 
}  }  }  }  }
 
}  }  }  }  }
 
</source>
 
</source>
В конфиге мы описали что надо запустить два экземпляра модуля Motor с разными параметрами. Как вы видите способ адресации GPIO мы не указали, он у нас унаследуется от модуля, а вот параметры GPIO_forward и GPIO_backward мы переопределили в соответствии со схемой подключения.
+
В конфиге мы описали, что надо запустить два экземпляра модуля MotorTest с разными параметрами: один для основного мотора drive_motor, другой для рулевого мотора rotation_motor. Как вы видите способ адресации GPIO мы не указали, т.к. нас устраивает значение по умолчанию, а вот параметры GPIO_forward и GPIO_backward мы переопределили в соответствии со схемой подключения.
  
 
Теперь у нас совсем все готово. Можно запускать.
 
Теперь у нас совсем все готово. Можно запускать.
 
 
<source lang=bash>
 
<source lang=bash>
 
python3 StartBubot scout_easy
 
python3 StartBubot scout_easy
 
</source>
 
</source>
  
Теперь можно открыть свой пользовательский интерфейс в браузере [http://localhost/ui/scout_easy http://localhost/ui/scout_easy] и попробовать. При первом запуске Вас попросят ввести логин и пароль - введите любые значения, по умолчанию права доступа к системе не установлены.
+
Открываем в браузере подготовленный нами пользовательский интерфейс [http://localhost/ui/scout_easy http://localhost/ui/scout_easy] и пробуем нажать на кнопку. При первом запуске Вас попросят ввести логин и пароль - введите любые значения, по умолчанию права доступа к системе не установлены.
 
 
Конфигуратор доступен по адресу [http://localhost/ui/studio http://localhost/ui/studio]
 
  
 
===Bubot scout===
 
===Bubot scout===
[[Файл:scout.png|400px|thumb|right|Bubot:scout - Схема подключения]]
+
Если поступательно развивать нашу машинку дальше, то следующий шаг это регулировка оборотов привода, замена рулевого мотора на серву, установка камеры, приводов на камеру, синтезатора речи, распознавание голосовых команд и т.д. Но это уже совсем другая история.
 
 
Если поступательно развивать нашу машинку дальше, то следующий шаг это регулировка оборотов привода, замена рулевого мотора на серву, установка камеры, приводов на камеру, синтезатора речи.
 
 
 
Из железа понадобятся сервы и контроллер для их подключения. На сервах экономить не стоит, самые дешевые просто невозможно было медленно поворачивать, их постоянно передергивало.
 
 
 
Контроллер я взял на базе PCA9685. Строка для поиска контролера на котом я остановился "16-Channel 12-bit PWM", обойдется Вам примерно в 12$ с доставкой.
 
 
 
 
 
 
 
Полный пример кода bubot scout входит в состав дистрибутива Bubot. Схематично сервисы там выглядят следующим образом.
 
 
 
[[Файл:scout_buject.png|400px|thumb|right|Bubot:scout - Схема сервисов]]
 
  
Предыдущий пример я специально сделал упрощенным. Реализуя более сложные алгоритмы имеет смысл определить интерфейсы для типовых устройств - Мотор, Серва. А уже их реализацию под конкретное железо - драйвера делать через наследование. В качестве примера можно посмотреть классы PCA9685Motor который по сути стал драйвером для моторов подключаемых через генератор шим - PCA9685.
+
[[Файл:scout_scheme.png|500px|Bubot:scout - Схема подключения]]
Также в отдельные модули я вынес работу i2c и GPIO чтобы не было проблем с блокировками и т.п.
+
[[Файл:scout_buject.png|500px|Bubot:scout - Схема сервисов]]
  
Более подробно где скачать, как установить фреймворк можно почитать тут. На очереди пример реализации на базе фреймворка контроллера умного дома.
 
  
 
[[Категория:Bubot]]
 
[[Категория:Bubot]]

Текущая версия на 01:24, 3 апреля 2015

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


Предыстория

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

Из наиболее популярных подходили Microsoft Robotics Studio и ROS, и все бы ничего, но на текущий момент привязать их к конкретному железу весьма не просто, разобраться в них с нуля так же не тривиальная задача, ну и самое главное писать на языке C очень не хотелось. Душа просила чего-нибудь по проще и по легче.

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

Концепция

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

Bubot scheme.png

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

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

Hello Bubot

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

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

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

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

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

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

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

Scout easy scheme.png

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

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

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

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

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

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

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

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

    # Метод вызывается перед запуском основго цикла
    # выставляем режим работы GPIO
    def on_ready(self):    
        GPIO.setmode(self.param["mode"])
        GPIO.setwarnings(False)

    # фреймворк при получения сообщения, вызывает одноименный метод для его обработки
    # передавая ему в качестве параметра все сообщение
    # данный модуль у нас будет уметь принимать только один запрос
    # set_power на установку мощности мотора в процентах    
    def incoming_request_set_power(self, message): 
        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": "MotorTest",
            "description": "название сервиса по умолчанию"
        },
        "parent": {
            "value": "Buject",
            "description": "название базового модуля, с которого наследуются другие параметры"
        },
        "buject": {
            "value": "MotorTest",
            "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/jquery-ui-1.11.2/jquery-ui.css">
    <script type="text/javascript" src="/static/jquery-2.1.3.min.js"></script>
    <script type="text/javascript" src="/static/jquery-ui-1.11.2/jquery-ui.js"></script>
    <script type="text/javascript" src="/static/bubot_socket.js"></script>
    <title>BuBot</title>
    <script>
        function bubot_on_open() {
            <!--функция должна быть определена на каждой странице,
            вызывается после открытия WebSocketa для того чтобы запросить у сервера
            начальные данные, пример использования можно посмотреть в ui/studio или ui/scout-->
        }

        function get_bubot_actions() {
            <!--функция должна быть определена на каждой странице, вызывается при загрузке страницы,
            содержит объект из функций для обработки входящих сообщений. 
            Пример использования можно посмотреть в ui/studio или ui/scout-->
            return {};
        }

        $(function () {
            <!-- вешаем обработчики на нашу кнопку-->
            $("#command_move_forward").button({}).mousedown(function () {

                <!--описание функции ниже по тексту статьи, 
                    при нажатии устанавливаем мощность мотора 100%
                    при отжатии устанавливаем мощность мотора в 0%-->
                bubot_send_message('send_request', {'name': "set_move_motor_power",
                                                    'data': {'value': 100}});
            }).mouseup(function () {
                bubot_send_message('send_request', {'name': "set_move_motor_power",
                                                    'data': {'value': 0}});
            });
        })
    </script>
</head>
<body class="ui-widget-content">
    <button id="command_move_forward" class="command_button">forward</button>
    <div id="console" class="ui-widget-content"></div>
</body>
</html>

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

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

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

\ui\scout_easy\scout_easy.json

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

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

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

В каталоге config хранятся конфиги всех Ваших роботов. Создавать конфиги можно как вручную, так и про помощи конфигуратора - но об этом подробнее на видео о bubot: scout. Итак, для нашего первого робота конфиг будет выглядеть следующим образом:

\config\scout_easy.json

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

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

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

python3 StartBubot scout_easy

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

Bubot scout

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

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