ROS, ELM и черепашка

    Robotic Operation System позволяет взаимодействовать своим подсистемам по механизмам «подписка на топик» и «вызов сервиса» по своему специальному протоколу. Но есть пакет rosbridge, который позволяет общаться с ROS извне с помощью websocket. Описанный протокол позволяет выполнять основные операции по взаимодействию с другими подсистемами.

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

    Я решил совместить приятное с полезным и изучать ROS (по которой сейчас идет курс) и ELM вместе.

    В ROS есть демонстрационный модуль turtlesim, эмулирующий робота-черепашку. Один из предоставляемых им узлов рисует движение черепашки в своем окне, другой — преобразует нажатия стрелок на клавиатуре в команды движения и поворотов черепашки. К этому процессу можно подключиться из простой программы на ELM.

    ELM использует паттерн model-updater-view. Состояние программы описывается типом данных Model, функция update берет входящие события типа Msg и преобразует старую модель в новую (и, возможно, операцию, которую надо выполнить), а функция view по модели строит ее представление в пользовательком интерфейсе, который может порождать события типа Msg. Еще события могут приходить по подпискам, которые создаются специальной функцией из модели.

    Обобщенная web-программа на ELM выглядит так:

    init : ( Model, Cmd Msg )
    update : Msg -> Model -> ( Model, Cmd Msg )
    view : Model -> Html Msg
    subscriptions : Model -> Sub Msg
    main =
      Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }
    

    а программисту остается только реализовать эти четыре функции.

    Опишем модель:

    type alias Model =
      { x : Float
      , y : Float                      -- координаты черепашки
      , dir : Float                    -- направление, в котором черепашка смотрит
    
      , connected : Bool          -- подключенность к серверу
      , ws : String                  -- URL websocket, который слушает rosbridge
                                       -- если ROS запущен на рабочей машине
                                       -- и все настроено поумолчанию,
                                       -- url будет ws://localhost:9090/
      , topic : String               -- топик, по которому управляется черепашка,
                                       -- обычно /turtle1/cmd_vel
    
      , input : String              -- JSON сообщение, которое мы можем редактировать
                                      -- и отправить в систему руками
      , messages : List String  -- Пришедшие со стороны rosbridge сообщения
                                       -- эти поля требуются только для отладки
                                       -- и в исследовательских целях
      }
    
    init : ( Model, Cmd Msg )
    init =
      ( Model 50 50 0 False "ws://192.168.56.101:9090/" "/turtle1/cmd_vel" "" []
      , Cmd.none
      )
    

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

    type Msg
      = Send String
      | NewMessage String
      | EnterUrl String
      | EnterTopic String
      | Connect
      | Input String
    

    Это так называемый алгебраический тип, описывающий прямую (размеченную) сумму нескольких альтернатив. Наиболее близкое предстваление этого типа в ООП — Msg объявляется абстрактным классом, а каждая строка алитернативы описывает новый, унаследованный от Msg, конкретный класс. Input, Send и прочее — это имена-конструкторы этих классов, за которыми следуют параметры конструктора, которые превращаются в поля класса.

    Каждая альтернатива это запрос на изменение модели и выполнение каких-либо операций, который порождается действиями пользователя с интерфейсом (view) или внешними событиями — получением данных из websocket.

    • Send String — запрос на отправку строки в websocket
    • NewMessage String — обработать принятую из websocket строку
    • EnterUrl String — редактируется url для websocket
    • EnterTopic String — редактируется топик
    • Connect — закончить редактирование настроек и связаться с сервером
    • Input String — редактирование «ручного» сообщения в websocket

    Теперь более-менее понятно, как реализовать функцию update:

    update : Msg -> Model -> ( Model, Cmd Msg )
    update msg model =
      case msg of
        EnterTopic newInput
         -> ( { model | topic = newInput }, Cmd.none )
        EnterUrl newInput
         -> ( { model | ws = newInput }, Cmd.none )
        Connect
         -> ( { model | connected = True }, WebSocket.send model.ws (subscr model.topic) )
        Input newInput
         -> ( { model | input = newInput }, Cmd.none )
        Send data
         -> ( { model | input = "" }, WebSocket.send model.ws data )
        NewMessage str
         -> case Decode.decodeString (decodePublish decodeTwist) str of
              Err _
               -> ( { model | messages = str :: model.messages }, Cmd.none )
              Ok t
               -> let ( r, a ) = turtleMove t.msg
                      dir = model.dir + a
                  in  ( { model
                        | x = model.x + r * sin dir
                        , y = model.y + r * cos dir
                        , dir = dir
                        , messages = str :: model.messages
                        }
                      , Cmd.none
                      )
    

    Здесь используются несколько функций, которые мы определим позднее:

    • subscr: String -> String — конструирует строку запроса для подписки на топик в rosbridge
    • (decodePublish decodeTwist) — декодирование сообщения от топика, содержащее данные ROS-типа geometry_msgs/Twist, с которыми оперирует черепашка
    • turtleMove: Twist -> ( Float, Float ) — извлечение из сообщения перемещения и угла поворота черепашки

    А пока определим функцию view:

    view : Model -> Html Msg
    view model =
      div [] <|
        if model.connected
        then let x = toString model.x
                 y = toString model.y
                 dirx = toString (model.x + 5 * sin model.dir)
                 diry = toString (model.y + 5 * cos model.dir)
             in  [ svg [ viewBox "0 0 100 100", Svg.Attributes.width "300px" ]
                     [ circle [ cx x, cy y, r "4" ] []
                     , line [ x1 x, y1 y, x2 dirx, y2 diry, stroke "red" ] []
                     ]
                 , br [] []
                 , button [ onClick <| Send <| pub model.topic 0 1 ]
                     [ Html.text "Left" ]
                 , button [ onClick <| Send <| pub model.topic 1 0 ]
                     [ Html.text "Forward" ]
                 , button [ onClick <| Send <| pub model.topic -1 0 ]
                     [ Html.text "Back" ]
                 , button [ onClick <| Send <| pub model.topic 0 -1 ]
                     [ Html.text "Rigth" ]
                 , br [] []
                 , input [ Html.Attributes.type_ "textaria", onInput Input ] []
                 , button [ onClick (Send model.input) ] [ Html.text "Send" ]
                 , div [] (List.map viewMessage model.messages)
                 ]
        else [ Html.text "WS: "
             , input
                 [ Html.Attributes.type_ "text"
                 , Html.Attributes.value model.ws
                 , onInput EnterUrl
                 ]
                 []
             , Html.text "Turtlr topic: "
             , input
                 [ Html.Attributes.type_ "text"
                 , Html.Attributes.value model.topic
                 , onInput EnterTopic
                 ]
                 []
             , br [] []
             , button [ onClick Connect ] [ Html.text "Connect" ]
             ]
    
    viewMessage : String -> Html msg
    viewMessage msg = div [] [ Html.text msg ]
    

    view создает DOM (можно чтитать, что просто html). Каждый объект (тег) генерируется отдельной функцией из библиотеки «elm-lang/html», которая принимает два параметра — список аттрибутов, типа Html.Attribute и список вложенных объектов/тегов. (Лично я считаю такое решение неудачным — я как-то поместил вложенный элемент в тег br и потом долго не мог найти его на экране, правильная библиотека не должна позволить сделать такую ошибку, оставив у br только аргумент с аттрибутами. Но возможно, в таком подходе есть глубокий смысл для специалистов во фронтетде.)

    Отдельно я хочу описать аттрибуты. Тип Html.Attribute — это сборная-солянка для совершенно разнородных сущностей. Например Html.Attributes.type_ : String -> Html.Attribute msg задает тип в таких тегах, как imput, а Html.Events.onClick : msg -> Html.Attribute msg задает событие, которое должно произойти при клике на этот элемент.

    Полностью прописать Html.Attributes.type_ в коде пришлось из за конфликта с Svg.Attributes.type_.

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

    onClick <| Send <| pub model.topic 0 1

    Он эквивалентен

    onClick (Send (pub model.topic 0 1))

    <| — это оператор применения функции к аргументу (в Haskell он называется '$'), который позволяет использовать меньше скобок.

    onClick — уже рассмотренная создания аттрибута, ее параметр — генерируемое событие.

    Send — один их конструкторов типа Msg, ее патаметр — строка, которую мы хотим потом отправить в websocket.

    Конструкторы и типы в ELM пишутся с большой буквы, а переменные (точнее константы и параметры функций), обычные и типовые, с маленькой.

    pub model.topic 0 1 — вызов функции создания запроса на отправку сообщения о движении черепашки на топик. Топик берется из модели, а 0 и 1 — перемещение и поворот.

    Опишем недостающие функции. Проще всего создавать сообщения для отправки в websocket, так как это просто строки:

    subscr : String -> String
    subscr topic = "{\"op\":\"subscribe\",\"topic\":\"" ++ topic ++ "\"}"
    
    pub : String -> Float -> Float -> String
    pub topic m r =
      "{\"topic\":\""
        ++ topic
        ++ "\",\"msg\":{\"linear\":{\"y\":0.0,\"x\":"
        ++ toString m
        ++ ",\"z\": 0.0},\"angular\":{\"y\":0.0,\"x\":0.0,\"z\":"
        ++ toString r
        ++ "}},\"op\":\"publish\"}"
    

    С обработкой сообщений немного сложнее. Тип сообщения, с которым работает turtlesim можно посмотреть средствами ROS:

    ros:~$ rosmsg info geometry_msgs/Twist
    geometry_msgs/Vector3 linear
      float64 x
      float64 y
      float64 z
    geometry_msgs/Vector3 angular
      float64 x
      float64 y
      float64 z

    rosbridge его превращает в json и заворачивает в сообщение о событии на топике.

    Декодирование его будет выглядеть так:

    type alias Vector3 = ( Float, Float, Float )
    
    type alias Twist = { linear : Vector3, angular : Vector3 }
    
    decodV3 : Decode.Decoder Vector3
    decodV3 =
      Decode.map3 (,,)
        (Decode.at [ "x" ] Decode.float)
        (Decode.at [ "y" ] Decode.float)
        (Decode.at [ "z" ] Decode.float)
    
    decodeTwist : Decode.Decoder Twist
    decodeTwist =
      Decode.map2 Twist
        (Decode.at [ "linear" ] decodV3)
        (Decode.at [ "angular" ] decodV3)
    
    type alias Publish a = { msg : a, topic : String, op : String }
    
    decodePublish : Decode.Decoder a -> Decode.Decoder (Publish a)
    decodePublish decMsg =
      Decode.map3 (\t m o -> { msg = m, topic = t, op = o })
        (Decode.at [ "topic" ] Decode.string)
        (Decode.at [ "msg" ] decMsg)
        (Decode.at [ "op" ] Decode.string)
    

    Декодер Json-представления некоторого типа комбинируется из других декодеров.
    Decode.map3 (,,) применяет три декодера, переданные ему в параметрах, и создает тупл из трех декодорованных элементов с помощью операции (,,).

    Decode.at декодирует величину, извлеченную по данному пути в Json заданным декодером.

    Код

    (\t m o -> { msg = m, topic = t, op = o })

    описывает замыкание. Он аналогичен коду на js:

    function (t,m,o) { return {"msg":m, "t":t, "op":p} }

    Полный код можно взять с github.

    Если есть желание попробовать ROS придется установить самостоятельно. Вместо установки ELM можно воспользоваться сервисом.
    • +12
    • 2,6k
    • 5
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 5
    • 0

      Большое спасибо за интересную статью. Это отлично, что появляются курсы и такие статьи по ROS'у. Крайне интересно узнать про ваш опыт:


      1. Возникли какие-либо сложности при установке/настройке ROS'a. Если да, то какие?
      2. Правильно ли я понял, что такой способ работы через rosbridge по функционалу полностью аналогичен нодe с Publisher and Subscriber?
      3. Как вы думайте, rosbridge подходит для полноценной работы с ROS? Не будет ли проблем со скоростью из-за отсутствия сериализации в бинарный протокол и использования WS вместо TCP или же область применения rosbridge ограничивается использованием в веб-интерфейсах для ручного управления и мониторинга?
      • 0
        Особых проблем не возникло. Дома я установил на ноут со старой ubuntu, на работе в виртуалке со свежей xubuntu. Была мелкая проблема с самой виртуалкой — 64-битная OS в ней не пошла, пошла только 32-битная версия, но если VirtualBox сконфигурирован под 64-битную ubuntu.
        Судя по описанию протокола можно так же использовать и создавать сервисы. Но я пока не пробовал.
        В документации я не нашел средств получания метаинформации — списков и описаний пакетов, топиков, типов, сервисов. А с ними бы из web-интерфейса было бы интересно поработать. Мне кажется, rosbrige вполне подойдет для мониторинга и управления (типа SCADA или чего-то похожего на виртуальную реальность) даже в промышленном применении. В учебном применении время реакции не столь критично, и на нем можно прототипировать и что-то более низкоуровневое. Я хочу сделать фреймворк для ELM, который бы позволял работать с ROS как из web, так и через nodejs. В последнем случае, вероятно, без UI. Надеюсь, это расширит область применимости ELM.
      • +1
        В документации я не нашел средств получания метаинформации — списков и описаний пакетов, топиков, типов, сервисов.

        Посмотрите этот проект: ros-control-center. Судя по скриншоту и описанию (ROS Control Center offers an easy way to show nodes, topics and service names.) им удается получать метаинформацию.


        По поводу мониторинга, у них ряд интересных проектов: robotwebtools


        В учебном применении время реакции не столь критично, и на нем можно прототипировать и что-то более низкоуровневое.

        Как понимаю, при использовании ROS, ноды находят друг друга через Master'a, а в дальнейшем коммуницируют напрямую. В случае использования rosbridge коммуникация происходит через ноду-посредника на котором запущен WS сервер. Чисто теоретически, при большом количество топиков и подписчиков, такая нода может стать узким местом. Вопрос, правда, на сколько это критично.

        • 0
          Попробую разобраться, как они это делают. Правда, они используют библиотеку, а я не очень хочу читать джаваскрипт.

          Я не очень представляю, как соединять ноды напрямую используя подписку на топики, то есть многие-ко-многим. Только для сервисов, но как я понял, они используются реже.
        • 0
          Я не очень представляю, как соединять ноды напрямую используя подписку на топики, то есть многие-ко-многим. Только для сервисов, но как я понял, они используются реже.

          При обычной (без rosbridge) работе с нодами, они уже коммуницируют напрямую (даже через PubSub) Once these nodes have located each other they communicate with each other peer-to-peer.. Судя по всему, каждая нода хранит у себя список подписчиков.

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