Sign In/My Account | View Cart  
advertisement


Listen Print

Autopilots in Perl
by Jeffrey Goff | Pages: 1, 2

Special types like "mph" and "deg" are stored in another hash reference, back on lines 81-85. When the time comes to display the actual data, we look into this hash reference to pull out the format string to use when sprintf()'ing the data. The len hash key gets used as well at this time, to create a mask of space characters that we use to erase the old value completely before displaying the new value.

  81 my $typedef = {
  82   deg => { format => "%+03.3f", len => 8 },
  83   mph => { format => "%03.3f", len => 7 },
  84   pct => { format => "%+01.3f", len => 6 },
  85 };

A special type bool is used for indicator lamps (since indicator lamps are either on or off), but is handled specially. The pseudo-types l and f aren't represented here, but are used when we need to return data to the simulator. While the simulator sends out only floating-point numbers, it receives a mixture of floating-point and integer values, and the mixture changes on a per-channel basis.

Instead of making the programmer create the format strings that we'll use later on to pack() and unpack() packets, we pre-compute them in the function create_pack_strings(). Since the individual elements may occur anywhere in the eight-element array, we may need to skip over elements, and that's done with liberal use of x4 in the format, which tells pack() and unpack() to ignore 4 bytes' worth of data.

 164 sub create_pack_strings {
 165   for my $row (values %$DATA_packet) {
 166     $row->{unpack} = 'x4';
 167     $row->{pack} = 'l';
 168     for my $j (0..DATA_max_element) {
 169       if(exists $row->{$j}) {
 170         my $col = $row->{$j};
 171         $row->{pack} .=
 172           (grep { $col->{type} eq $_ } @float_formats) ? 'f' : 'l';
 173         $row->{unpack} .= 'f';
 174       }
 175       else {
 176         $row->{pack} .= 'f';
 177         $row->{unpack} .= 'x4';
 ...

Starting at line 164, create_pack_strings() handles this tedious job for us, by walking the two-dimensional hash reference $DATA_packet. Line 166 starts the unpack() string with x4, which tells the unpack() function to skip over the index long in the packet. We have to unpack the index beforehand in order to know how to deal with the data, so we just ignore that.

Line 167 starts the pack() string with a long, l for the inbound index. Lines 168 onward create the individual elements. The unpack() strings are f if the element is in use, x4 if it's not. This means that the format strings only extract the data we need, which makes it easier later on when time comes to actually call unpack() on the actual data.

Lines 171-2 and 176 create the pack() format string, using f for floating-point formats and l for integer types. Since there's no special way to tell the simulator what elements we're updating, we have to send back every element. Unused elements are filled with a sentinel value of -999 to say "Do not update this value."

In the end, we've added pack and unpack hash keys to every channel in our $DATA_packet structure. Unpacking a channel structure with this format returns to us only the data we're interested in, and skips over unused elements so we don't have to account for empty elements in the array that pack() returns to us.

Likewise, the pack hash key gives us a string to create an entire channel's worth of data, with the proper data types. This is important, even in what should be a simple channel like the gear and brake display. While gears get set to an integer 1 or 0, brakes have to be set to a float from 0 through 1, to account for variable pressure.

Pulling Apart the Packet

All of the heavy lifting gets done in the receive_DATA() function, from lines 193-234. This function accepts the message sent over the UDP port and breaks it into individual channel-sized packets. The adventure starts on line 196, after clearing a small internal buffer we use to record the last packet received.

  196   for (my $i = 0;
  197        $i < (length($message)-&DATA_header_size-1) / DATA_element_size;
  198        $i++) {
  199    my $channel = substr($message,
  200                         $i * DATA_element_size + DATA_header_size,
  201                         DATA_element_size);

Line 197 computes the number of channels this particular message contains (we're doing this on-the-fly in case you want to change the channel selection while the simulator is running.) The substr() call breaks the message into chunks of DATA_element_size bytes, and gives us back a channel's worth of data.

  203    my $index = unpack "l", $channel;
  204    next unless exists $DATA_packet->{$index};

Next, we extract the index (the first byte) of the channel so that we can unpack the data appropriately. If we don't know anything about this channel (i.e., if it isn't present in the $DATA_packet hashref), we reject it and move on. This makes us somewhat immune to changes in format.

  206    my $row = $DATA_packet->{$index};
  207    my @element = unpack $row->{unpack}, $channel;

Next, we get back the elements we're interested in here by unpacking with the format string that got calculated in create_pack_strings(). The format string skips the index and unpacks just the elements we wanted. So, now we walk our proffered hash and extract the individual elements:

  208     my $ctr = 0;
  209     for my $j (0..DATA_max_element) {
  210       next unless exists $row->{$j};
  211       my $col = $row->{$j};
  212       $DATA_buffer->{$index}{$j} = $element[$ctr];

Line 208 initializes a counter so that we can keep track of which element we've extracted. Line 209 loops through each possible element in order, so we take into account the possibility that we haven't used a data element.

Lines 210-212 skip unused elements in the sparse array, and saves the content in $DATA_buffer, so we can have keys like g toggle the gear setting, rather than having one key to raise the gear and one key to lower them.

Finally, we display each element based on its type. Boolean types are displayed as either [#] or [ ] depending upon whether the element is a floating one or zero. They're still written out as an integer, but displayed as floating.

Types such as deg and mph with a registered format are handled specially. We first wipe out the old data completely by overwriting with spaces. This prevents a potential issue with the old value being "3.14159", and the new value being "2.7". If we didn't overwrite the old value, it would be displayed as "2.74159", with the extra digits remaining from the old display.

If we've not been told what to do with this (which is the case with Frame Rate), simply print the value and go onward.

Data-Driven Design

In the current monitor program, each channel in a DATA packet corresponds to a set of fields onscreen. Instead of creating a function to display the onscreen labels and another one to extract the information from the packets, we've elected to combine the label information with the channel and field, in one easy-to-use format.

Since the number of channels and their layout varies between versions of the simulator, we've chosen to represent the channels in a two-tiered hash reference. The outer tier represents a channel, and the inner tier represents the fields inside that channel.

This means that if a channel changes index number (as the pitch and roll channel did from version 7.1 to 7.4), we simply update the channel number in the hashref rather than cutting out a block of code and repositioning it in an array, making maintenance easier.

Since this data likely won't change over the lifetime of the program, we'll store it in a global hash reference, $DATA_packet starting at line 92. We can reference a given element in an arbitrary channel with the code fragment $DATA_packet-{$channel}{$element}>, but the code usually ends up iterating by channel and by element.

The sample for the gear display starts at line 109, and the entire channel/element reference looks like:

  109  12 => {
  110    0 => { type => 'bool', label => 'Gear',
  111           label_x => 0, label_y => 3,
  112           x => 6, y => 3 },
  113  },

This is the only element in channel 12, and sits at element 0. Boolean types are displayed with [#] and [ ] representing indicator lamps, and it specifies the screen coordinates of the label (label_x and label_y) and where the actual indicator goes (x and y).

After Curses initializes, the setup_display() function iterates over this two-tiered hash and draws the label strings that won't change. Lines 148-156 take care of this, and show how to iterate over the data structure:

  148 sub setup_display {
  149   for my $channel (values %$DATA_packet) {
  150     for my $element (values %$channel) {
  151       $win->addstr($element->{label_y},
  152                    $element->{label_x},
  153                    $element->{label}) if $element->{label};
  ...

Note that $win-addstr()> takes the Y coordinate followed by the X coordinate, in accordance with long-standing tradition. Later on, we'll use the type hash key to tell us how to display this data, but that's handled when we receive a message.

Initialization and Shutdown

We start by handling the usual command-line options on lines 24-32, including a -h option to display usage. -x sets the X-Plane IP address to something other than the default of "127.0.0.1," -r changes the port the monitor listens on from 49999 to something else.

Incidentally, the listening port cannot be 49000, as that's where the simulator listens for its commands. -t tells the monitor to transmit on a different port than 49000, although the simulator is documented to listen to only port 49000. -d is there in case this gets run with earlier versions than 7.4, where the packet format varied depending upon the operating system the simulator was running on.

After command-line configuration is processed and defaults overridden, we create the UDP sockets, on lines 48-65. Instead of the usual TCP protocol, we open UDP ports as that's what the simulator communicates with. If we did this after initializing Curses, our error text would be eaten by the terminal, so we place this first.

Startup and Shutdown of Curses

The Curses startup proceeds fairly normally starting on line 314, with the call to noecho() and cbreak() stopping the terminal from echoing key presses and suppressing carriage returns. $win->timeout(1); lets us read key presses without blocking, so we can display packets as they come in real-time without having to wait for key presses.

Displaying Text

Since Curses implementations vary widely in functionality, we limit ourselves to making addstr() and getch() calls to ensure maximum compatibility across platforms. Inside setup_display() we draw the static labels such as "Gear" and "Pitch," and do the job of displaying the actual values inside receive_DATA().

The Main Loop

This makes the main loop on lines 301-311 pretty straightforward. We poll the keyboard, and display the latest data packet if there isn't one. Otherwise, check to see if the user pressed "q", in which case we quit. UDP sockets don't require any special tear down, so all that's left is to call endwin();. If the user pressed a command key, create an appropriate packet and send that out.

With just the DATA packet, you can create your own customized cockpit display, even take this sample source and turn it into a Gtk application that lets you monitor your plane graphically. The VEHA packet type adds even more possibilities. You could read a friend's virtual location and add his plane as traffic in your virtual world.

Even better, scrape HTML from a flight-tracker service and add real traffic to your virtual world! The FAIL and RECO packet types can cause simulated system failures, so you can create your own in-flight emergencies in Perl! You can even go all the way, and use the SNAP packet type to completely override X-Plane's flight model, telling X-Plane how you think the aircraft should fly.

Hopefully now that I've demystified some of X-Plane's internal workings you'll be inspired to create your own tools, maybe even design and build your own fly-by-wire plane, all in Perl.