ROS, ELM and Turtle

    Robotic Operation System allows you to interact with their subsystems according to the "subscription to topic" and "service call" mechanisms according to their special protocol. But there is a rosbridge package that allows you to communicate with ROS from the outside using websocket. The described protocol allows you to perform basic operations to interact with other subsystems.

    ELM is a very simple and elegant language compiled in javascript and is great for developing interactive programs.

    I decided to combine business with pleasure and learn ROS (which is currently being taught ) and ELM together.

    ROS has a turtlesim demo moduleemulating a turtle robot. One of the nodes provided by him draws the movement of the turtle in his window, the other converts the pressing of the arrows on the keyboard into commands of movement and rotation of the turtle. You can connect to this process from a simple ELM program.

    ELM uses the model-updater-view pattern. The state of the program is described by the Model data type, the update function takes incoming events of the Msg type and converts the old model into a new one (and, possibly, the operation that needs to be performed), and the view function builds its representation on the model in the user interface, which can generate events of the Msg type . Events can also come from subscriptions that are created by a special function from the model.

    A generalized ELM web program looks like this:

    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
        }
    

    and the programmer can only implement these four functions.

    We describe the model:

    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
      )
    

    So far, nothing complicated, the model is a structure with named fields.
    The Msg type is less common for OO programmers:

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

    This is the so-called algebraic type, describing the direct (labeled) sum of several alternatives. The closest representation of this type in OOP - Msg is declared an abstract class, and each line of the alternative describes a new, specific class inherited from Msg. Input, Send, and more, are the constructor names of these classes, followed by constructor parameters that turn into class fields.

    Each alternative is a request to change the model and perform any operations that is generated by user actions with the interface (view) or external events - receiving data from websocket.

    • Send String - request to send a string to websocket
    • NewMessage String - process a string received from websocket
    • EnterUrl String - edit url for websocket
    • EnterTopic String - edited topic
    • Connect - finish editing the settings and contact the server
    • Input String - editing a “manual” message in websocket

    Now it’s more or less clear how to implement the update function:

    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
                      )
    

    Several functions are used here, which we will define later:

    • subscr: String -> String - constructs a query string for subscribing to a topic in rosbridge
    • (decodePublish decodeTwist) - decoding a message from the topic containing the ROS data type geometry_msgs / Twist with which the turtle operates
    • turtleMove: Twist -> (Float, Float) - extract from the message the movement and angle of rotation of the turtle

    In the meantime, define the view function:

    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 creates a DOM (you can read that just html). Each object (tag) is generated by a separate function from the “elm-lang / html” library, which takes two parameters - a list of attributes, such as Html.Attribute and a list of nested objects / tags. (Personally, I consider this decision unsuccessful - I somehow placed the nested element in the br tag and then could not find it on the screen for a long time, the correct library should not allow such an error, leaving only the argument with the attributes for br. But perhaps in such the approach has a deep meaning for specialists in the front-end.)

    Separately, I want to describe the attributes. The Html.Attribute type is a hodgepodge for completely heterogeneous entities. For example, it Html.Attributes.type_ : String -> Html.Attribute msgsets the type in tags such as imput, and Html.Events.onClick : msg -> Html.Attribute msgsets the event that should occur when a user clicks on this element.

    Html.Attributes.type_ had to be fully written in the code due to a conflict with Svg.Attributes.type_.

    Consider a piece of code that can be hard to read:

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

    It is equivalent

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

    <|Is the operator of applying a function to an argument (in Haskell it is called '$'), which allows you to use fewer brackets.

    onClick- the creation of the attribute already considered, its parameter is the generated event.

    Send- one of the constructors of type Msg, its parameter is the string that we want to send to websocket later.

    Constructors and types in ELM are written with a capital letter, and variables (more precisely constants and function parameters), ordinary and standard, with a small one.

    pub model.topic 0 1- call the function to create a request to send a message about the movement of the turtle on the topic. The topic is taken from the model, and 0 and 1 - movement and rotation.

    We describe the missing functions. The easiest way is to create messages to send to websocket, as these are just strings:

    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\"}"
    

    Message processing is a bit more complicated. The type of message turtlesim works with can be viewed using 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 turns it into json and wraps it in a message about the event on the topic.

    Decoding it will look like this:

    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)
    

    A Json representation decoder of some type is combined from other decoders.
    Decode.map3 (,,)It uses three decoders passed to it in the parameters, and creates a tuple of three decoded elements using the operation (,,).

    Decode.atdecodes the value extracted along a given path in Json by a given decoder.

    The code

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

    describes the closure. It is similar to js code:

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

    Full code can be taken from github .

    If you want to try ROS, you will have to install it yourself. Instead of installing ELM, you can use the service .

    Also popular now: