⬆️ ⬇️

ROS, ELM and Bug

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



ELM is a very simple and elegant language that compiles to javascript and is great for developing interactive programs.



I decided to combine business with pleasure and study ROS (which is currently taking the course ) and ELM together.



ROS has a demo module turtlesim , which emulates a robot bug. One of the nodes provided by him draws the movement of the turtle in his own window, the other one converts pressing the arrows on the keyboard into commands for moving and turning the turtle. You can connect to this process from a simple ELM program.

')

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



Generalized web-program on ELM 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 ) 


While 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, which describes the direct (marked) 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 class inherited from Msg. Input, Send, and so on are the constructor names of these classes, followed by the constructor parameters that turn into class fields.



Each alternative is a request to change the model and perform any operations, which is generated by the actions of the user with the interface (view) or external events - receiving data from the 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 ) 


Here we use several functions that we will define later:





For now, 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 library “elm-lang / html”, which takes two parameters - a list of attributes, such as Html.Attribute and a list of nested objects / tags. (Personally, I consider such a decision unsuccessful - I somehow put 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 making such an error, leaving only an attribute argument with br. But maybe in this There is a deeper meaning for specialists in the frontline.)



Separately, I want to describe the attributes. The Html.Attribute type is a hodgepodge for completely dissimilar entities. For example, 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 you click on this item.



Fully write Html.Attributes.type_ in the code was due to a conflict with Svg.Attributes.type_.



Consider a piece of code that may be difficult to understand:



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


It is equivalent to



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


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



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



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



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



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



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



 subscr : String -> String subscr topic = "{\"op\":\"subscribe\",\"topic\":\"" ++ topic ++ "\"}" pub : String -> Float -> Float -> String pub topic mr = "{\"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\"}" 


Handling messages is a bit more complicated. The type of message that 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 an event message on a 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 (\tmo -> { msg = m, topic = t, op = o }) (Decode.at [ "topic" ] Decode.string) (Decode.at [ "msg" ] decMsg) (Decode.at [ "op" ] Decode.string) 


A decoder of a Json representation of some type is combined from other decoders.

Decode.map3 (,,) applies three decoders passed to it in 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.



Code



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


describes a closure. It is similar to the js code:



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


The full code can be taken from github .



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

Source: https://habr.com/ru/post/340534/



All Articles