Howto:Making HTTP Requests from Nasal

From FlightGear wiki
Jump to navigation Jump to search


Objective

Demonstrate how to make HTTP requests from FlightGear without touching the C++ code, just by editing some XML/script files, in order to connect FlightGear to a web service and exchange arbitrary data (including FlightGear properties) between a web server and FlightGear.

Prerequisites

  • Understand how the property tree works
  • Understand how basic XML files work (PropertyList-encoded XML file)
  • Understand basic Nasal scripting (setprop/getprop, props.nas, fgcommands)
  • Access to a server to host your server-side script (php, ruby, python, jsp etc)


The xmlhttprequest fgcommand

Update 10/2013: As of FlightGear 2.99+, Nasal will have cppbind-based HTTP client support flightgear/flightgear/cf270bde229994f1171d9742e4aeb31a4eaa3ccd commit view.

Earlier FlightGear versions didn't include Nasal socket support for security reasons.

However, FlightGear versions >= 2.8 include a new feature to make HTTP requests using a new xmlhttprequest fgcommand. So, Nasal is able to execute so called "fgcommands", these are internal FlightGear commands implemented in C++ This can be used to implement AJAX-like server/client exchanges.

You can pass any FG property tree data (lat,lon,alt,groundspeed etc) via conventional GET requests.

The request will be made asynchronously, in order not to block the main loop or affect the frame rate. This is why you should register a listener to invoke a callback upon completion of the request and check the status flag (200=OK).

If you are familiar with Java's ActionListeners or JavaScript or NodeJS, this will should be a very familiar concept.

The server-side response

The only "restriction" is that the server-side response (the output from your CGI script) must be a PropertyList-encoded XML file so that FlightGear can deal with it and directly load it into its property tree for further processing.

A minimal (empty) valid server-side response that can be processed by FlightGear looks like this:

<?xml version="1.0"?>
<PropertyList>
</PropertyList>

In between the PropertyList root tags, you can include arbitrary other data in a simple tag/value format (without depending on attributes, and preferring separate tags instead, to simplify processing via the FG property tree and the Nasal scripting language):

<?xml version="1.0"?>
<PropertyList>
 <!-- this is all custom stuff using a <node>value</node> format -->
  <status>ok</status>
  <code>400</code>
  <message>Hello World</message>
 <!-- end of custom data -->
</PropertyList>

All of these tags (status, code, message) are arbitrary and just made up. It is up to FG (your Nasal code) to process the server response further (or simply ignore the response completely).

Note that it is even perfectly possible to respond using an XML GUI dialog, or even arbitrary Nasal code. So that your server-side script may for example send a procedurally created dialog to FlightGear, that may even be dynamically created at the server-side.

Making a request

You can simply run the fgcommand like this:

fgcommand("xmlhttprequest");

Obviously, that doesn't do anything useful, because we haven't specified any URL yet - this is done separately, using a 2nd function argument:

fgcommand("xmlhttprequest", params);

Where "params" should be a props.Node object, (see $FG_ROOT/Nasal/props.nas):

var params = props.Node.new();
fgcommand("xmlhttprequest", params);

The props.Node() hash needs to be initialized with two required fields:

  • url - HTTP request URL
  • targetnode - destination in the local property tree where the server's response will be written to

(For details, please see the comments below about the C++ code implementing the xmlhttprequest fgcommand) So that a complete example may look like this:

Request Signals

In addition to the "url" and "targetnode" parameters, the following signals can be set up to be signalled by the xmlhttprequest command

  • complete
  • failure
  • status

These must be explicitly set to point to a request specific property.

For additional details, please refer to the source code and the implementation of do_load_xml_from_url() and the RemoteXMLRequest class: flightgear/flightgear/next/src/Main/fg_commands.cxx#l1174

Using Python to create simple test server

This section is aimed at people who don't have access to a server environment.

For testing purposes, you may want to use python to set up a simple local test server first that serves a static response in the form of a simple PropertyList XML file:

  • create a new folder, change into the folder
  • add a new PropertyList-encoded XML file inside the folder named response.xml:
<?xml version="1.0" encoding="UTF-8"?>

<PropertyList>

<message>Hello World!</message>

</PropertyList>

First tests

Once everything works as expected, use this code in FlightGear (e.g. via the Nasal Console):

var hash = {"url": "http://localhost:8000/response.xml", "targetnode": "/server-response"};
var params = props.Node.new( hash );
fgcommand("xmlhttprequest", params);

A more concise version may read:

var params = props.Node.new( {"url": "http://localhost:8000/response.xml", "targetnode": "/server-response"} );
fgcommand("xmlhttprequest", params);

After executing this portion of code, you'll find the response under /server-response in the property tree


Or even:

fgcommand("xmlhttprequest", props.Node.new( {"url": "http://localhost:8000/response.xml", "targetnode": "/server-response"} ) );

To use the whole thing in a timer/listener callback, you could create a helper function:

var sendRequest = func {
fgcommand("xmlhttprequest", props.Node.new( {"url": "http://localhost:8000/response.xml", "targetnode": "/server-response"} ) );
}

settimer(sendRequest, 5.0);

Alternatively, you can also directly use an anonymous function:

settimer(func(){
    fgcommand("xmlhttprequest", props.Node.new({"url": "http://localhost:8000/response.xml", "targetnode": "/server-response"}));
}, 5.0);


It's probably a good idea to make some simple experiments first, i.e:

  • start FGFS
  • Open the Nasal Console
  • do a simple HTTP request to your server, using the following code

For testing purposes, you could paste this into your Nasal Console and customize things for your own setup:

# customize this and change it to your URL:
var url = "http://localhost:8000/response.xml?foo=1&bar=2";
# this sets up the parameters for the xmlhttprequest fgcommand
# (targetnode is the destination where the PropertyList-encoded response
# is copied to)
var params = props.Node.new( {"url": url, "targetnode": "/fse-response"} );
# make the actual HTTP request
fgcommand("xmlhttprequest", params);

The server-side response will be directly written into the FlightGear property tree, so that it can be further processed or inspected using the built-in Property browser.

Sending data from FG to the server

In general, it's always a good idea to pass a handful of parameters to your server-side script, such as for example:

  • FlightGear version
  • API version (for the server/client exchange)
  • callsign
  • aircraft (model name)

All of these are readily available in the FlightGear property tree.

It's a good idea to look at existing dialogs in $FG_ROOT/gui/dialogs to look up useful property names.

The following snippet demonstrates how to create a new request type that responds to pausing/unpausing the simulator:

var Notify = func(event) { 
return func fgcommand("xmlhttprequest",
props.Node.new(
{ 
  url: "http://localhost:8000/response.xml?event="~event,
  targetnode: "/server-response/",
}
));
}

setlistener("/sim/freeze/master", Notify("freeze"));

Now, whenever you pause/unpause the sim, you'll see a corresponding server request being made:

localhost "GET /response.xml?event=freeze HTTP/1.1" 200 -

You can also easily include other data from the FlightGear Property Tree. You can use the internal "variable browser" to see what data is readily available in FG: Property browser.

Note that some properties may be aircraft-specific, so it' safer to use the general purpose properties if you want your script to work for all aircraft:

var lat=getprop("/position/latitude-deg");
var lon=getprop("/position/longitude-deg");
var url = "http://localhost:8000/response.xml?lat="~lat~"&lon="~lon;
var req_params = props.Node.new( {"url": url, "targetnode": "/fse-response"} );
fgcommand("xmlhttprequest", params);

Keep in mind that URL encoding (parameters) may need to be handled separately:


Now, to implement a simple "events" system, you can use listeners and invoke the httprequest in turn. FlightGear uses the concept of a property tree: Property tree. This is global variable tree that contains numbers, strings etc

You can register so called "listeners" that are function callbacks which are automatically executed when a property is modified: Using listeners and signals with Nasal.

So, you only need to add a listener for the parking brake property (i.e. from the 777) and the callback will be executed automatically In turn, the callback could be using the xmlhttprequest command to send a request to your server.

Requests, Events and Automation

You can automate the sending of such requests by using Nasal's support for timers and listeners. Timers will be invoked after a certain delay, while listeners will be invoked once a certain property is modified - such as for example the parking brake. Obviously, these two features can also be combined so set up listeners that invoke timers or vice versa.


For example, to send a HTTP request once the simulator is paused, you can use this snippet:

var Notify = func (event) {
 var url = "http://www.example.com/script.php?event="~event;
 var req_params = props.Node.new( {"url": url, "targetnode": "/fse-response"} );
 fgcommand("xmlhttprequest", params);
}
setlistener("/sim/freeze/master", func Notify("pause") );

To send a request using a timer, use a timer instead of a listener:

  settimer(func Notify("test-timer"), 5);

This will send the "test-timer" event every 5 seconds to the url specified in the Notify() function.

To set up a listener that fires timer, which in turn sends the event after 5 seconds, use something like this:

setlistener("/sim/freeze/master", 
  func settimer( Notify("paused-5s-ago"), 5)
);

Example: An OOP Request Wrapper

You can also use a simple class as a wrapper for all request-related features. For example, something like this:

# create a simple Request hash with two fields
 var Request = {url:, targetnode:};
 # create a constructor that returns new Request objects
 Request.new = func(url, dest) return {parents:[Request], url:url~"?", targetnode:dest};
 # add a new method to the Request class to append GET parameters to the url

 Request.addArg = func(name, value) {
  me.url ~= "&" ~name~ "=" ~value;
  return me;
 }
 # actually use the me.url and me.targetnode fields to make the object-specific request
 Request.send = func fgcommand("xmlhttprequest", props.Node.new( {url:me.url, targetnode:me.targetnode} ));
 
# This is how you can use the Request class
# Note that this uses an anonymous Request object and method chaining
# the func in front of the 2nd setlistener argument is needed to wrap
# everything into a closure
setlistener("/sim/freeze/master", func 
  Request.new("http://localhost:8000/response.xml","/server-response")
  .addArg("message", "Hello World")
  .send() 
);

When you copy/paste this into your Nasal console, then execute it and pause/unpause the simulator, you'll see corresponding HTTP request being made:

localhost:8000/response.xml?&message=Hello World

This makes it very easy to create new Request objects, and they can be just as well used with timers, too:

settimer( func 
   Request.new("http://www.fseconomy.com/script.jsp","/fse-response")
  .addArg("message", "Hello World")
  .send() 
  , 5.0
);

Example: A Request Class that supports Properties

You can also easily extend the Request class such that it supports sending properties:

# create a simple Request hash with two fields
 var Request = {url:, targetnode:, _props:[] };
 # create a constructor that returns new Request objects
 Request.new = func(url, dest) return {parents:[Request], url:url~"?", targetnode:dest};
 # add a new method to the Request class to append GET parameters to the url

 Request.addArg = func(name, value) {
  me.url ~= "&" ~name~ "=" ~value;
  return me;
 }

 Request.addProp = func(p) { 
  append( me._props, p);
  return me;
 }

 Request.update = func foreach(var p; me._props) me.addArg(p,getprop(p));

 # actually use the me.url and me.targetnode fields to make the object-specific request
 Request.send = func { 
  me.update();
  fgcommand("xmlhttprequest", props.Node.new( {url:me.url, targetnode:me.targetnode} ));
 }

 
# This is how you can use the Request class
# Note that this uses an anonymous Request object and method chaining
# the func in front of the 2nd setlistener argument is needed to wrap
# everything into a closure

setlistener("/sim/freeze/master", func 
  Request.new("http://localhost:8000/response.xml","/server-response")
  .addProp("/position/latitude-deg")
  .addProp("/position/longitude-deg")
  .addProp("/position/altitude-ft")
  .send() 
);

At the server side, the request will look like this:

localhost - - "GET /response.xml?&/position/latitude-deg=13.97338914228831&/position/longitude-deg=166.6434527689312&/position/altitude-ft=500.0000000000001&/position/latitude-deg=13.97338914228831&/position/longitude-deg=166.6434527689312&/position/altitude-ft=500.0000000000001 HTTP/1.1" 200 -

Processing the server response

The server's response will be written to the property tree location specified via "targetnode". Afterwards, the property signal set via the "complete" field of the props.Node hash will be triggered, so that a callback can be invoked.

From then on, you can use all conventional FlightGear (Nasal) functions to parse the response, i.e. using getprop()/setprop() or the props.nas module in $FG_ROOT/Nasal.

This can then be used to set up session-specific data after a login/password "handshake" has taken place.

Example: Implementing Server-driven GUI dialogs

All valid <PropertyList>-encoded XML files are also valid server-side responses that can be processed by FlightGear. This includes most FlightGear XML files, such as GUI dialogs, joystick/keyboard configuration etc

FlightGear's whole GUI is fully scriptable. All the GUI dialogs are written in XML with embedded Nasal script. One of the simplest examples is in fact our "exit" dialog: flightgear/fgdata/next/gui/dialogs/exit.xml

The GUI dialogs are all stored in $FG_ROOT/gui/dialogs There are tons of examples to be found there. The GUI system is documented in $FG_ROOT/Docs/README.gui: $FG_ROOT/Docs/README.gui

New dialogs can be easily created by borrowing stuff from existing dialogs and copying/pasting things together. Lots of GUI-related helpers can be found in $FG_ROOT/Nasal/gui.nas

Technically, there are no restrictions - i.e. you can refer to existing dialogs, or even make up completely new ones. Not only at the client side, but also at the server-side. To demonstrate how this works, here's a little example which:

  • downloads an existing PropertyList-encoded dialog (about.xml)
  • renames the dialog to "foo"
  • registers the dialog with FlightGear's GUI system
  • and finally shows the dialog to the user

Note that dialogs may also include embedded Nasal code, so that it can also be created/modified procedurally:


var listener_id = nil;

# the location in the property tree where we will store the server's response
var response_dest = "/server-response/dialog/";
# the "signal" property that will be set upon completion of the download
# which we'll use to register a callback
var completed = "/server-response/complete";

# clean the destination tree and remove all children
# i.e. leftovers from earlier runs
props.globals.getNode( response_dest,1 ).getParent().removeChild('dialog', 0);
 
# set up the xmlttprequest hash with the URL we'll use
# in this case we'll just download the "about" dialog 
# from the master repository
# The "complete" property will be triggered upon completion
# of the download
var hash = {
  "url": "https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/gui/dialogs/about.xml", 
  "targetnode": response_dest,
  "complete": completed
};
 
# put the parameters in a new props.Node hash
var params = props.Node.new( hash );
 
# now, make the actual HTTP request (async)
fgcommand("xmlhttprequest", params);
 
# this is the callback that we will register
# so that it gets invoked upon completion of the download

var success = func {
print("Download completed!");

# next, we rename the downloaded dialog, so that the GUI system
# doesn't just load the standard about dialog from disk but uses
# the new dialog data instead

setprop(response_dest~"name", "foo" );
 
# register the "new" dialog with the GUI system
fgcommand("dialog-new", props.globals.getNode(response_dest, 1) );
 
# finally, show the registered GUI dialog
gui.showDialog("foo");
print("Finished");
removelistener(listener_id);
}
 
listener_id=setlistener(completed, success);

As can be seen, this technique can be used to implement fully interactive server/client exchanges that are GUI driven.

Example: Running remotely served Code

FlightGear doesn't care about the content of the server-side response as long as its provided in a valid PropertyList-encoded form. In other words, you can also serve dynamic content, such as Nasal code and execute it at the client side.

Demonstrates how to use the xmlhttprequest to download Nasal code from a server and run it at the client side (fgfs)

Adding a new menu item to the menubar

Open $FG_ROOT/gui/menubar.xml and add this to one of the menus (such as the Multiplayer) menu:

                <item>
                        <name>fseconomy</name>
                        <binding>
                                <command>nasal</command>
                                <script><![CDATA[
                                        if (contains(globals, "FSEconomy")) {
                                                gui.popupTip("Loading FSEconomy module");
                                        } else {
                                                gui.popupTip("FSEconomy module already loaded", 5.0);
                                        }
                                ]]></script>
                        </binding>
                </item>

Next, open $FG_ROOT/Translations/en/menu.xml and add translation string for the "fseconomy" handle. Using for example the "Multiplayer" menu:

<!-- Multiplayer menu -->
        <multiplayer>Multiplayer</multiplayer>
        <mp-settings>Multiplayer Settings</mp-settings>
        <mp-chat>Chat Dialog</mp-chat>
        <mp-chat-menu>Chat Menu</mp-chat-menu>
        <mp-list>Pilot List</mp-list>
        <mp-carrier>MPCarrier Selection</mp-carrier>
        <fseconomy>FSEconomy</fseconomy>

Serving Nasal code to FG clients

Next, create a new PropertyList-encoded XML file that contains the code you want to run (which will be served by the server to all fgfs clients) and save it as code.xml (this could be just as well a CGI instead of a static file):

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<PropertyList>
  <code><![CDATA[
   gui.popupTip("Hello World from the server", 5.0);
  ]]></code>
</PropertyList>

Running downloaded Code

Now, open the menubar.xml file again and change it such that it downloads the file from your server and executes the code section found in the response, for example:

 <item>
<name>fseconomy</name>
<binding>
<command>nasal</command>
<script><![CDATA[
var listener = nil;
var completed = "/fse-response/completed";
var params = {url:"http://localhost:8000/code.xml", targetnode:"/fse-response", complete: completed};
fgcommand("xmlhttprequest", props.Node.new(params));

var run = func {
 var code = getprop("/fse-response/code");
 compile(code, "fse module") ();
 removelistener(listener);
}

listener=setlistener(completed, run);

]]></script>
</binding>
</item>

Please keep in mind that this will directly invoke all code. So you need to be careful when using this with already running code (listeners, timers, loops) - and terminate such code first. Otherwise, you'll have muliple instances of the code running at the same time.


Instead of directly calling compile() here, it would be better to create a new namespace and bind the function to it-so that the namespace can be properly cleaned up.

The C++ Implementation

There are plans being discussed to extend the fgcommand further and make it more flexible [1]. This provides a little overview on the implementation of the xmlhttprequest fgcommand and is aimed at people who want to work with the C++ code to extend the xmlhttprequest implementation.

The xmlhttprequest fgcommand is implemented in $FG_SRC/src/Main/fg_commands.cxx#l1174, the patch implementing xmlhttprequest can be seen here flightgear/flightgear/2218a44ed76def7dcbe9233cfd34488ad66b7752 commit view.

The fgcommand uses a RemoteXMLRequest helper class [2].

This is currently implemented such that it will directly try to deal with the response as an PropertyList-encoded XML file and load it into the property tree.


The parent/interface class is simgear::HTTP::Request - the headers are to be found in <simgear/io/HTTPRequest.hxx>

You may also need to look at:

demo code (unit tests) can be found in:

Nasal APIs

There is also a built-in Nasal HTTP module for doing HTTP requests

http.load()

Load resource identified by its URL into memory.

var request = http.load(<url>);
http.load("http://example.com/test.txt")
    .done(func(r) print("Got response: " ~ r.response));

http.save()

Save resource to a local file.

var request = http.save(<url>, <file_path>);
http.save("http://example.com/test.png", getprop('/sim/fg-home') ~ '/cache/test.png')
    .fail(func print("Download failed!"))
    .done(func(r) print("Finished request with status: " ~ r.status ~ " " ~ r.reason))
    .always(func print("Request complete (fail or success)"));

Related