Howto:Port I/O from Nasal

From FlightGear wiki
Jump to navigation Jump to search

Introduction

Cquote1.png there's no built-in functionality to allow Nasal access to serial ports
— geneb (Feb 25th, 2016). [Flightgear-devel] Nasal & Serial I/O....
(powered by Instant-Cquotes)
Cquote2.png

You can't really 'access' a device from Nasal directly. What you'll most likely want to do is define a generic protocol for the properties you're interested in sending between fgfs and your device. A generic protocol allows you to easily import external data into FlightGear's property tree and export properties, too.

You can configure FlightGear to use that protocol directly with your serial device, through network to secondary program which handles serial, etc.

Of course, in FlightGear, you can then use Nasal scripting to handle and modify the properties being sent/received by whatever logic you desire.

This feature alone makes it possible to circumvent many restrictions of the generic protocol, so that the protocol spec itself just boils down to transmitting and receiving a single "string" property, which in turn is entirely controlled and modified from scripting space.

So just because there's no low level "function" available to do something from Nasal, that doesn't necessarily mean it cannot be done. This doesn't just apply to networking I/O or serial port access: There's also no dedicated Nasal command to play sounds, control AI traffic, place clouds etc - yet, we can do all of these things, by leveraging dedicated subsystems which are invoked/interfaced to via the property tree.

You would just be delegating the I/O job to a dedicated sub system and merely use the property tree system to set and get the data to be processed. So, Nasal wouldn't even (need to) know that it is processing data that's coming from an external source. Thus, Nasal doesn't even need low level port access. All these low level details are handled by a separate subsystem, without you having to handle the details from scripting space.

In Nasal, you could use a conventional setprop() call to set a property, which is then picked up by the I/O subsystem and sent to its destination (e.g. serial port/network).

 setprop("/myoutput-property/data", "my data");

Obviously, you need a matching generic protocol that picks up the corresponding properties, marshals them into the required format and sends them to their destination. The connection details are entirely handled by FlightGear, so they need to be specified during start up time.

Setting up and using a generic protocol is very easy, just a simple XML file defining the property values you want to send and/or receive...with optional formatting. FlightGear can then use this protocol with various end points from simple logging to file, across the network, directly to/from a serial device, etc...via a simple command line option.

There are many FlightGear users who manage to create I/O protocols and who manage to interface to FG using network sockets or serial port I/O, without even knowing what sockets or UARTs really are, how they internally work, let alone how to cater for the differences across the plethora of platforms supported by FlightGear.

All of these things are internally handled by the generic protocol system and abstracted away by the FlightGear property tree, where hardware I/O is just a matter of dealing with the FlightGear property tree.

This is due to the power of the subsystems that are largely configurable, usable and accessible using XML and the property tree - there's is often no need for any low level "hard coding" (i.e. in C or C++ space).

Basically, FlightGear allows you to make use of features that would take even an experienced C++ developers hours or even weeks to recreate in C++ space, and this is without even touching the core C++ source code.

Accessing a serial port by coming up with a generic protocol is simple and well-documented.

We have a plethora of examples, too - if you'd like to copy/paste stuff from different places. Then, all you need to do is to come up with input and output properties for transmitting your data. Once this is working, you can look into coming up with a piece of Nasal to further process the properties as required.

A generic protocol is really nothing more than a list of properties to send and/or receive. Nothing complex to develop and think about, just what properties are of importance to you. Also don't worry about Nasal at this point, it is not necessary for sending/receiving from generic protocol which just reads/writes from the property tree. Thinking about Nasal at this point is just muddying the water...forget about it until you actually need it.

For example: just imagine the simplest protocol that you can come up with which simply sends a string to some other host, once you are able to modify this string from Nasal space, you can also implement entire protocols on top of the property tree without it even being aware of it.

Now, all you need is some simple generic protocol that sends your "output property" to your target device/host:

<!-- $FG_ROOT/Protocol/nasal-io.xml -->
<?xml version="1.0"?>
<PropertyList>
<generic>
   <output>
      <line_separator>newline</line_separator>
      <var_separator>tab</var_separator>
       <chunk>
         <name>eufd property</name>
         <node>/sim/cockpit/eufd</node>
         <type>string</type>
         <format>%s</format>
       </chunk>
   </output>
</generic>
</PropertyList>

Now, to send a message, you would only need to modify the property - for example, by using setprop:

setprop('/sim/cockpit/eufd', 'Hello World');

But you could also create a wrapper named "sendMessage" for this purpose:

var sendMessage = func(msg) {
 print("Sending message:", msg);
 setprop('/sim/cockpit/eufd', msg);
}

Now, you could also create additional wrappers to do device-specific things, like clearing the LCD screen, setting the font color, font size, positioning the cursor etc, simply by adding device specific commands to each wrapper:

var setFontColor = func(color) {
 sendMessage("0xFF,0xAC,0xEF....");
}
var setHeading = func(heading) {
 sendMessage("0xFF,0xCA,0xFA,$"~heading);
}

The same concept could also be used to come up with an "input property" to process results from your device. You could then register a listener that is automatically invoked whenever the "input property" is modified, this will automatically invoke your Nasal callback (i.e. a Nasal function) so that you can process the input.

var processMessage = func() {
  print("Info: Message received!");
}

_setlistener("/sim/cockpit/eufd-input", processMessage);

Obviously, you could now add some header to each message to identify the message type. For example, by checking a substring of the message:

var processMessage = func() {
  var msg = getprop('/sim/cockpit/eufd-input');
  var msg_type = substr(msg,0,1);
  print("Info: Message received! TYPE:", msg_type);
}

_setlistener("/sim/cockpit/eufd-input", processMessage);

Next, you could come up with a list of supported message types:

var processMessage = func() {
  var msg_types = {'A':'SET FONT','B':'CLEAR SCREEN','C':'RESET','D':'SHUTDOWN','E':'NOP'};
  var msg = getprop('/sim/cockpit/eufd-input');
  var msg_type = substr(msg,0,1);
  print("Info: Message received! TYPE:", msg_type);
  if ( contains(msg_types,msg_type) ) print(msg_types[msg_type]);
}

_setlistener("/sim/cockpit/eufd-input", processMessage);

You can also directly use hexadecimal values:

  var ESC=0x32;
  var ACK=0x13;
  var NAK=0x11;

See generic protocol for more info on the generic protocol and have a look in $FG_ROOT/Protocol for examples of various protocol configurations. There's also a howto tutorial available here: Howto: Create a generic protocol.

We suggest you just try it, create a simple protocol with a couple properties, explore ways to send and receive those values, and so on. If your serial device is a microcontroller that your programming then you may be able to get away with sending/receiving the values directly to/from flightgear without need of any secondary 'middleware' programs at all. If not you may need a small program to handle marshalling the data between flightgear and your hardware.

On the PC side of things I would strongly suggest forgetting about C/C++ unless you really, really prefer it and/or have no choice. It is going to be much more complex doing serial and/or network programming in straight C/C++.

Using a serial device properly requires more than just 'fopen'ing it, except in the most simple of cases, and can be quite tedious and complex using straight C system calls...networking as well. Personally I recommend using a higher level language (I use Python) that will make your life so much easier and allow you to get things done quickly and easily without hassles of low level details.

For your data processing needs (formatting, conversion, marshalling, compression etc), you can use a combination of the sprintf() library function, and the $FG_ROOT/Nasal/bits.nas file.

 var output_property = '/sim/cockpit/eufd';
 var data = sprintf( "offset:%x", 255 );
 setprop(output_property, data);

References