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

Материал из razgovorov.ru
Перейти к: навигация, поиск
(Общая структура пользовательского модуля.)
Строка 107: Строка 107:
  
 
</source>
 
</source>
 
+
===Общая структура конфига пользовательского модуля.===
Общая структура конфига пользовательского модуля.
+
<source lang=javascript>
===<source lang=javascript>===
 
 
{
 
{
 
     "param": {
 
     "param": {

Версия 06:36, 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_ready(self):
        # метод вызвается перед запуском процесса.
        # в этот момент все параметры сервиса проиницилизированы
        # небходимые для старта процессы уже запустились
        # процесс подписан на все необходимые сообщения
        return

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

    def main_loop(self):
        # основной цикл процесса, частота срабатывания определяется параметром max_fps.
        # отрицательное значение max_fps задает частоту не чаще одного раза в Х секунд
        # метод автоматически один раз в мекунду обновляет параметры и статус модуля в разделяемой памяти
        # большой fps существенно увеличивает нагрузку на процессор из-зи redis
        # если Вам нужен основной цикл с fps>30 имеет смысл вынести его в систему реального времени,
        # например на базе 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": "В комнате сработал датчик движения"
        }
    }
}

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

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

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

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

{
    "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": { // список пользователей имеющих доступ в веб-интерфейс
    }
}

Конфиги роботов должны располагаться в каталоге 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>

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

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 где находятся автогенерируемые файлы.