Guile driver

From The Player Project

Revision as of 21:11, 19 May 2011 by Newchief (Talk | contribs)
Jump to: navigation, search

This page is about guile driver. This driver is capable to execute provided script written in Scheme language. Since this is a message-driven driver, provided script is started each time a message is received by a driver.

Contents

Simplicity of Scheme language

Scheme is a functional language derived from Lisp. At first sight it can be noticed that Scheme program is full of parentheses. This is because all function calls are written inside of parenthesis: (function-name arg1 arg2 arg3), and if any argument of given function is a result of evaluation of another function, nested parantheses can be used: (function-name (another-function some-arg) arg2 arg3). Note that there's no difference between function call and use of arithmetic operator: (display (+ 3 (* 2 1))). This is called Polish notation. Most things in Scheme looks like function call, for example, condition (with comparison of two numbers):

(cond ((> 1 3) (display "something is wrong, one should not be greater than three") (newline))
      (else (display "one is not greater than three, this is OK") (newline)))

This language uses s-expressions that use parentheses to organize lists. These lists are used to hold hierarchically organised data structures. A list can be nested in another list and so on, for example (1 2 ((3 4) (5)) 6), just like nested tags in XML, except that s-expressions are much much simpler. Note that Scheme program itself is a list. It is worth to mention that most of Scheme functions were written for lists processing.

Nice thing about Scheme comparing to Lisp is that there is only one function (called 'define') for defining things (variables and functions), while in Lisp we have one function per each type of definition. There are more differences like this that makes Scheme easier to learn and use.

Guile is one of the most popular Scheme interpreters. It is available for most (if not all) of UNIX-like operating systems. Since it also provides libguile library which offers nice API, it is easy to embed this Scheme interpreter into your own C/C++ program. The guile driver for Player makes use of that.

How does it work?

On each message arrival a function defined as follows will be executed:

(define fname (lambda (link hdr data env)
  (your-code)
))

('lambda' here is a special Scheme function that returns given Scheme code as a new function so we can use 'define' to bind name to it; first parameter for 'lambda' is a list of this new function parameters; function parameters have no defined types)

Such a function can be considered as a think-act part of sense-think-act loop. Using 'scriptfile' or 'script' configuration file option, function body can be provided (see configuration file options below).

When you write 'your-code', it will be placed inside of function definition as shown above. The parameters passed to this functions should be used to deal with incoming message data. These are:

  • link - communication link, you need to provide it to most of Player functions described below
  • hdr - data header, using appropriate Player functions you can get information about incoming message (sender, message type, data type)
  • data - the content of incoming message, use Player functions to decode it
  • env - working environment (can be a list of anything)

The 'env' parameter is the value returned by previous call of such a function. At first call, 'env' parameter is the value returned by initialization code (see 'globals' configuration file option). If there's no initialization code, the initial value for 'env' is an empty list.

Typically, such a function should decode message, then having message data and informations from working environment, should make a decision (send Player command to another Player device), finally it should construct and return new working environment.

Player functions

  • (player-match-message header msg_type msg_subtype key) - it looks at given header (see message processing function parameters) and returns true if it matches given criteria:
    • msg_type - one of: player-msgtype-data, player-msgtype-cmd, player-msgtype-req, player-msgtype-resp-ack, player-msgtype-synch, player-msgtype-resp-nack or -1 for any type
    • msg_subtype - search Player Manual for subtype codes (in most cases it is 1), -1 for any
    • key - while configuring required/provided intetface, you can assign key name to them - these keys are used for naming global variables representing declared interfaces in guile driver; pass such a variable as a parameter here to find out if received data message came from interface identified by given key name
  • (player-hdr-type header) - returns given header type (one of: player-msgtype-data, player-msgtype-cmd, player-msgtype-req, player-msgtype-resp-ack, player-msgtype-synch, player-msgtype-resp-nack)
  • (player-hdr-subtype header) - returns given header subtype
  • (player-hdr-timestamp header) - returns given header timestamp (a real number)
  • (player-publish link key msg_type msg_subtype data) - sends data message to given provided interface; 'key' parameter indicates to which provided interface data should be sent
  • (player-publish-timestamped link key msg_type msg_subtype data timestamp) - sends data message to given provided interface with given timestamp (which is a real number)
  • (player-publish-ack link key msg_subtype) - send ACK reply to given provided interface
  • (player-publish-nack link key msg_subtype) - send NACK reply to given provided interface
  • (player-putmsg link key msg_type msg_subtype data) - send command message to given subscribed interface; 'key' parameter indicates to which subscribed interface command should be sent
  • (player-putmsg-timestamped link key msg_type msg_subtype data timestamp) - send command message to given subscribed interface with given timestamp (which is a real number)
  • (player-forwardreq link key hdr data) - forward request to given subscribed interface; 'key' parameter indicates to which subscribed interface command should be sent
  • (player-create-datapack link type_name pairlist) - create new object (for example it can be a command to send by 'player-putmsg') of type given by name; pairlist parameter is a list of pairs (field-name . value), where 'value' can be a literal value or a result of nested 'player-create-datapack' call; see examples below
  • (player-read-datapack type_name field_name data) - decode value from given field of given datapack; field and type of datapack are given by names; note that for fields of array type other functions described below should be used; see examples below
  • (player-read-datapack-elem type_name field_name data offset) - this function is used for datapack fields which are known to be array of data; 'offset' is the 0-based index of required element
  • (player-read-datapack-elems type_name field_name data offset count) - this function is used for datapack fields which are known to be array of data; 'offset' is the 0-based index of first required element, 'count' is the number of required elements (useful for dealing with map and camera data)

Dealing with requests

Since this is not a threaded driver, messages of request type should be handled carefully. In most cases it is sufficient just to forward them. Following code will send ACK to request sender (here called 'input') and forward request message to another receiver (here called 'output'):

(cond
  ((player-match-message hdr player-msgtype-req -1 input)
    (player-publish-ack link input (player-hdr-subtype hdr))
    (player-putmsg-timestamped
      link
      output
      player-msgtype-req
      (player-hdr-subtype hdr)
      data
      (player-hdr-timestamp hdr)
    )
  )
)

This approach is not the best one since the reply sent back by final request receiver is not forwarded to the original sender (which always gets ACK). However, this example shows how requests can be handled anyway.

Another approach is to use player-forwardreq function:

(cond
  ((player-match-message hdr player-msgtype-req -1 input)
   (player-forwardreq link output hdr data)
  )
)

Configuration file options

  • keys (string tuple)
    • default value: NONE (certain value must be provided explicitely)
    • variable names for provided/required interfaces
    • note that they are global for all guile driver instances in one Player server
  • globals (string tuple)
    • default value: '()
    • global definitions, custom initialization Scheme code
    • each string in this tuple is one line of Scheme code
  • scriptfile (string)
    • default value: ""
    • file with Scheme code executed on each message arrival
  • script (string tuple)
    • default value: NONE
    • if scriptfile is not given, this tuple should contain lines of Scheme code
    • each string is one line of Scheme code
  • fname (string)
    • default value: NONE
    • if given, message processing function will have this explicit name
    • this name is global and should be unique
    • if not given, some unique name will be assigned

Some simple example

First let's configure (in Player server configuration file) velcmd driver that will be sending position commands - these commands will be received by guile driver and it will make our script alive:

driver
(
  name "velcmd"
  provides ["opaque:0"]
  requires ["position2d:0"]
  px 0.4
  py 0.0
  pa 0.0
  alwayson 1
)

The velcmd driver tells to go straight ahead at speed 0.4m/s, guile driver makes it to turn left/right randomly. See how random number generator is initialized in 'globals':

driver
(
  name "guile"
  fname "some"
  keys ["input" "output" "sensor"]
  provides ["input:::position2d:0"]
  requires ["output:127.0.0.1:6672:position2d:0" "sensor:127.0.0.1:6672:bumper:0"]
  globals [
    "(define get-time (lambda () (round (/ (get-internal-real-time) internal-time-units-per-second))))"
    "(let ((time (gettimeofday))) (set! *random-state* (seed->random-state (+ (car time) (cdr time)))))"
    "`(,(get-time) 0.0 0.0 0.0 1.0)"
  ]
  script [
   "(let*"
   "  ("
   "    (warn (cond"
   "               ((player-match-message hdr player-msgtype-data 1 sensor) (not (zero? (apply + (player-read-datapack-elems 'player_bumper_data 'bumpers data 0 (player-read-datapack 'player_bumper_data"
   "                                                                                                                         'bumpers_count data))))))"
   "               ((player-match-message hdr player-msgtype-data 1 output) (not (zero? (player-read-datapack 'player_position2d_data 'stall data))))"
   "               (else #f)"
   "    ))"
   "    (new-px (cond"
   "                 ((player-match-message hdr player-msgtype-cmd 1 input) (player-read-datapack 'player_pose2d 'px (player-read-datapack 'player_position2d_cmd_vel 'vel data)))"
   "                 (else (cadr env))"
   "    ))"
   "    (timedout (> (abs (- (get-time) (car env))) 0))"
   "    (change (or warn timedout))"
   "    (direction (cond ((and warn timedout) (* -1.0 (car (cddddr env)))) (else (car (cddddr env)))))"
   "    (new-time (cond (change (get-time)) (else (car env))))"
   "    (px (cond (change new-px) (else (caddr env))))"
   "    (pa (cond (change (- (random:uniform) 0.5)) (else (cadddr env))))"
   "  )"
   "  (cond"
   "       ((player-match-message hdr player-msgtype-req -1 input) (player-forwardreq link output hdr data))"
   "       (else"
   "            (player-putmsg link output player-msgtype-cmd 1"
   "              (player-create-datapack link 'player_position2d_cmd_vel"
   "                `((vel . ,(player-create-datapack link 'player_pose2d `((px . ,(* px direction)) (py . 0.0) (pa . ,pa))))"
   "                  (state . ,player-enable)"
   "                 )"
   "              )"
   "            )"
   "       )"
   "  )"
   "  `(,new-time ,new-px ,px ,pa ,direction)"
   ")"
  ]
)

The environment in this case is a list of five elements: time of last direction change (so the robot won't turn too frequently), new walk direction to take after timeout, current walk direction, current turn angle.

Dynamic code exchange capabilities

Erlang programming language (another functional language, like Scheme or Lisp but with completely different syntax) offers powerful infrastructure which seem to be something similar to Player (due to its message passing nature), except that it does not have any robotics related drivers and is too far from hardware to replace Player where it fits best. One of the most interesting features of Erlang language (and Erlang is full of interesting features) is the ability to dynamic code exchange at run time. With guile driver it is possible to achieve similar thing. Using 'lambda' and 'set!' functions it is possible do redefine function at run time in Scheme. Using 'eval-string' it is possible to update some function code with new code written in plain text string (provided string shoud use 'set!' and 'lambda' to indicate what and how should be replaced). The 'opaque' Player interface can be used to send strings with new Scheme code from one guile driver to another - potentially one driver can update code of another one.

Limitations

  • don't write time consuming programs - guile driver runs in main Player server thread, complicated script will slow down whole Player server (the one shown above is not complicated and is not slowing down anything)
  • avoid recursie calls to message processing function
  • most of Player API constants are missing - at present I have no ideas how to extract them from C/C++ Player source code automatically; Player API should have more general and easy to parse definition.

See also

Further reading

Personal tools