Bubot в деталях

Материал из razgovorov.ru
Версия от 19:34, 4 апреля 2015; Разговоров Михаил (обсуждение | вклад) (Пример логики страницы)
(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск

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

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

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

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

Базовый класс Buject предоставляет разработчику возможности хранения параметров в общей памяти, доступ к параметрам других модулей, систему обмена сообщениям.

Любой Buject состоит из двух файлов, один файл это собственно код на языке Python 3, а другой это конфиг модуля описывающий его назначение, параметры, методы и список зависимостей.

Общая структура пользовательского модуля.

from buject.Buject import Buject
# импортируем необходимые модули питон, как минимум модуль базового класса


class BujectName(Buject):
# Buject Name  - название Вашего класса может быть любым
# название не должно пересекаться с названиями  других модулей и буботов
# В качестве базового класса может выступать как сам базовый модуль Buject
# так и любой из его потомков.
# Например для всех моторов создан класс Motor, который задает общий интерфейс для работы с данным типом устройств
# а для подключения мотора скажем через контроллер PCA9685 мы создаем свой модуль унаследованный от модуля Motor
# в котором реализуем интерфейс применительно к конкретному железу. Своеобразный драйвер устройства.
    def __init__(self, user_config=None):
        super(BujectName, self).__init__(user_config) # читаем конфиги
        self.my_const_x = 'XXX' # определяете необходимые Вам константы.
        # все настраиваемые параметры должны быть описаны в конфиге


    def on_run(self):
        # метод вызывается сразу после запуска процесса
        return


    def on_ready(self):
        # метод вызывается перед запуском основного цикла.
        # в этот момент все параметры сервиса инициализированы
        # необходимые для старта процессы уже запустились
        # процесс подписан на все необходимые сообщения
        return

    def main_loop(self):
        # основной цикл процесса, частота срабатывания определяется параметром max_fps.
        # отрицательное значение max_fps задает частоту не чаще одного раза в Х секунд
        # метод автоматически один раз в мекунду обновляет параметры и статус модуля в разделяемой памяти
        # большой fps существенно увеличивает нагрузку на процессор из-за redis
        # если мощности Вашего процессора не хватает требуемый fps, то имеет смысл вынести этот алгоритм в систему реального времени,
        # например на базе arduino, которой в свою очередь управлять из модуля. 

        
        return

    def on_terminate(self):
        # метод вызывается после остановки сервиса
        return

    def incoming_event_XXX(self, message):
        # мeтод вызывается при поступлении события XXX
        return

    def incoming_request_XXX(self, message):
        # мeтод вызывается при поступлении запроса XXX
        return

    def incoming_response_XXX(self, message):
        # мeтод вызывается при поступлении ответа за запрос XXX
        return

    def action_XXX(self):
        # Вы можете определить любые Ваши методы, хорошим тоном будет использование префикса,
        # чтобы другие разработчики могли отличать пользовательские методы от методов фреймворка.

        buject_config = self.config
        # Вам доступны все параметры Вашего конфига в виде словаря
        # для более удобного использования они продублированы в малых словарях
        # param, status, bubot

        bubot_param   = self.bubot
        buject_param  = self.param
        buject_status = self.status

        # малые словари автоматически обновляются в разделяемой памяти и доступны другим сервисам
        # особое внимание следует уделить служебным статусам характеризующим текущее состояние сервиса
        buject_status = self.status['buject']  # статус ready, error, wait depend buject
        buject_acton  = self.status['action']  # чем в данный момент занимается сервис

        self.send_status()
        # принудительно обновление параметров и статусов сервиса в разделяемой памяти

        self.get_status('bubot', 'buject')
        # запрос параметров и статусов любого сервиса

        # фреймворк предоставляет следующие мотоды для обмена информацией между сервисами
        self.send_event('event_name', {'param_name':'param_value'})  #генерация события

        self.send_request('request_name', {'param_name':'param_value'})
        # отправка запроса, в метод передается только название и передаваемые параметры
        # информацию о получателе запрос и названии запроса у получателя метод получит из конфига

        self.send_response('request_data', 'response_data')
        # отправка ответа на запрос,
        # в метод передается запрос и данные для ответа
        # запрос нужен для определения получателя ответа

        return

Общая структура конфига пользовательского модуля.

{
    "param": {
        "name": {
            "value": "BujectFAQ" // имя сервиса по умолчанию, обязателен
            // если в системе будет запущен только один экземпляр этого модуля (сервис),
            // то этого имени будет достаточно
        },
        "buject": {
            "value": "BujectFAQ" // имя модуля, обязательно
        },
        "parent": {
            "value": "Buject" // имя базового модуля, обязательно
        },
        "user_parameter_1": {  // имя пользовательского параметра
            "value": "default value", // значение по умолчанию
            "description": "Пользовательский параметр 1", // описание параметра
            "type": "int"  // тип, используется при изменении значения параметра
            // через пользовательский интерфейс.
            // поддерживаются int (приведение строки из поля ввода к числу).
            // если type отсутствует то введенное в поле ввода значение
            // будет сохранено как строка
        }
    },
    "status": { // список параметров характеризующих состояние сервиса
        // в отличие от параметров они никак не влияют на работу сервиса
        // их значение вычисляется в процессе работы сервиса
        // характерным примером статуса является FPS
        "user_status_1": {  // имя пользовательского статуса
            "value": "default value", // инициализируется значением по умолчанию
            "description": "user status 1", // описание статуса
            "type": "unixtime"  // тип, используется при визуализации значения
            // в пользовательском интерфейсе.
            // поддерживаются unixtime (форматирование в строку при выводе)
            // если type отсутствует то значение выводится как есть
        }
    },
    "depend_buject": {// список необходимых для запуска модулей
        "PCA9685": {} // например данный процесс не будет запущен пока не стартует
                      // процесс с именем PCA9685 (status.buject = 'ready')
    },
    "incoming_request": { // список входящих запросов которые может обрабатывать модуль
        "set_power": { // название запроса
            "description": "Установить мощность мотора", // описание
            "param": { // список параметров принимаемых запросом, используется при отладке
                       // и конфигурировании робота через веб интерфейс
                "param_name": { // состав и назначение аналогично param, см. выше
                    "value": "default value",
                    "description": "Пользовательский параметр 1",
                    "type": "int"
                },
            "response": 1 // указывается для информирования о том, что метода может быть направлен ответ
            }
        }
    },
    "outgoing_request": { // список запросов отправляемых сервисом
        "set_power_move_motor": { // название запроса исходя из смысла действия, Установи мощность заднего мотора
            "bubot":  "other bubot", // указывается, если получателем запроса является другой бубот
            "buject": "move_motor", // название сервиса получателя запроса, обязателен
            "name": "set_power", // название запроса на стороне получателя, обязателен
            "description": "Устанавливаем мощность заднего мотора",
            "response": 1 // указывается если сервис будет ожидать ответа на этот запрос
        }
    },
    "outgoing_event": { // список событий происходящих в модуле
        "move_detected": { // название события
            "description": "Обнаружено движение", // описание
            "param": { // список параметров события, используется при отладке
                       // и конфигурировании робота через веб интерфейс - необязателен
                "param_name": { // состав и назначение аналогично param, см. выше
                    "value": "default value",
                    "description": "Пользовательский параметр 1",
                    "type": "int"
                }
            }
        }
    },
    "incoming_event": {  // список событий которые необходимы сервису для работы
        "room_move_detected": { // название события
            "bubot":  "other bubot", // указывается, если получателем запроса является другой бубот
            "buject": "room_control", // название сервиса получателя запроса, обязателен
            "name": "move_detected", // название сообщения на стороне инициатора
            "description": "В комнате сработал датчик движения",
            // если названия bubot и buject опущено, то имеется ввиду сообщения текущего бубота
            "param": { // список параметров события, используется при отладке
                       // и конфигурировании робота через веб интерфейс - необязателен
                "param_name": { // состав и назначение аналогично param, см. выше
                    "value": "default value",
                    "description": "Пользовательский параметр 1",
                    "type": "int"
                }
        }
    }
}

В конфиге модуля можно указывать только те параметры или методы которые отличаются от его предков.

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

Список будущих процессов их параметры и взаимосвязи описываются в конфиге робота. По структуре он практически идентичен конфигу модуля. Рассмотрим его по подробнее.

Структура конфига робота.

{
    "param": {
        "name": {
            "value": "BujectFAQ" // имя робота, должно быть равно имени файла
        }
    },
    "depend_buject": {// список сервисов и их параметры которые будут запущены
        "PCA9685": { // название сервиса, содержимое объекта содержит описание запускаемого процесса аналогично описанию модуля, указываем только параметры значения которых отличаются от значений по умолчанию
            "param": { // следующий ниже следующий пример запустит на базе модуля BujectFAQ процесс с именем Test, в котором от значений по умолчанию будет отличаться только один user_parameter_1
                "name": {"value": "Test"},
                "buject": {"value": "BujectFAQ"},
                "user_parameter_1": { "value": "33"}
            }
	} 
    },
    "user": { // список пользователей имеющих доступ в веб-интерфейс, если раздел не указывать то доступ не контролируется
        "Businka": { // Логин, 
            "password": "" // Пароль, можно не указывать
        }
    }
}

Конфиги роботов должны располагаться в каталоге config. В качестве примера можно посмотреть конфиг scout.

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

  1. Получаем параметры процесса
  2. Запускаем процесс
  3. Проверяем наличие методов для обработки входящих запросов и событий, оформляем подписку на соответствующие очереди в redis
  4. Ждем готовности процессов перечисленных в depend_buject.
  5. Сообщаем о своей готовности.
  6. Запускаем основной цикл.
  7. Каждая итерация цикла начинается с последовательной обработки всей очереди сообщений накопившейся с предыдущей итерации, после чего управление передается в main_loop.

Пользовательский интерфейс

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

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

Пример разметки страницы

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/static/jquery-ui-1.11.2/jquery-ui.min.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.min.js"></script>
    <!--bubot_socket - обязателен для каждой страницы - отвечает за обмен с сервером-->
    <script type="text/javascript" src="/static/bubot_socket.js"></script>
    <!--организация скриптов и  стилей внутри страницы остается на Ваше усмотрение-->
    <script type="text/javascript" src="/ui/FAQ/FAQ.js"></script>
    <title>BuBot</title>
</head>
<body class="ui-widget-content">
    <div id="playlist" class="ui-widget-content"></div>
    <!--console - служебный див - при наличии в него выводится консоль-->
    <div id="console"  class="ui-widget-content"></div>
</body>
</html>

Пример логики страницы

В примере разметки страницы вся логика вынесена в FAQ.js Рассмотрим его в качестве примера:

function bubot_on_open() {
// функция вызывается после успешного соединение / пересоединения с сервером
// как правило здесь следует делать запросы к серверу на получение начальных данных
//  в этом примере мы запрашиваем список музыкальных файлов
    bubot_send_message('get_playlist', null);
}
function get_bubot_actions() {
// содержит список функций которые могут быть вызваны на клиенте по инициативе сервера
// например в данном примере таким способом приходит ответ на запрос get_playlist
// аналочисным способом строися подписка на события и запросы сервисов
//
    return {
        on_get_play_list: function (data) { // получили с сервера список музыки и выводим его на странице
            var pl = $("#playlist");
            pl.empty();
            for (var elem in data) {
                pl.append("<div id='PL_" + elem + "' class='playlist ui-widget-content'>" + data[elem] + "</div>");
            }
            $(".playlist").mousedown(function(){
//                при клике на песне отправляем общее событие, если на него подписан сервис воспроизведения
//                то он запустит или остановит воспроизведение соответствующего трека
                bubot_send_message('send_event', {'event': "play", 'data': {'value': $(this).text()}});
            });
        }
    }
}

Пример конфига страницы

Пример модуля страницы

import os

from engine.BubotWebSocket import BubotWebSocket


class FAQ(BubotWebSocket): 
    # все сообщения приходячщие из браузера обрабатываются
    #  одноименныи методами с префиксом ui_ 
    def ui_get_playlist(self, param=None): 
        _files = os.listdir("./playlist/")
    
        # все сообщения отправляемые на клиент обрабатываются там одноименными функциями
        self.send_message_to_ui('on_get_play_list', {'param': _files})

    def on_open(self):
        # вызывается при установке соединения
        return

    def on_close(self):
        # вызывается при разрыве соединения,
        #  например можно остановить или вернут машину назад
        super().on_close() # обязательно вызываем родительский метод


Ограничения:

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