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:
and the programmer can only implement these four functions.
We describe the model:
So far, nothing complicated, the model is a structure with named fields.
The Msg type is less common for OO programmers:
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.
Now it’s more or less clear how to implement the update function:
Several functions are used here, which we will define later:
In the meantime, define the view function:
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_ 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:
It is equivalent
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.
We describe the missing functions. The easiest way is to create messages to send to websocket, as these are just strings:
Message processing is a bit more complicated. The type of message turtlesim works with can be viewed using ROS:
Rosbridge turns it into json and wraps it in a message about the event on the topic.
Decoding it will look like this:
A Json representation decoder of some type is combined from other decoders.
The code
describes the closure. It is similar to js code:
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 .
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 msg
sets the type in tags such as imput, and Html.Events.onClick : msg -> Html.Attribute msg
sets 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.at
decodes 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 .