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.

