This is a work in progress. For discussion and suggestions, please use
the Aerowinx
Forum.
Updates
- 2022-07-04: AA-beta-03 Renamed the module file to all lowercase
to better adhere to the Python recommendations. Couple of other Python
coding style changes. Added DEMAND mode initiator with Lexicon support.
Changed the MCDU head controller, in preparation of a more capable MCDU
support library based on higher-level specifications and templates.
- 2020-07-12 Added
PSX.py , a fully modular version
of the previous code, that can be used with import . Still
need to document it, but it is very comparable to the demo. Jump down to read the instructions.
Python Resources
- Main Python Web site
where you can download the interpreter and find everything else.
- Python 3 documentation
is your go-to resource for nearly all questions on what Python is and how
to use it to write programs.
PSX Resources
- PSX
Networking is the official reference to all PSX network details and
the list of available variables.
Explanation of the Python Code
I wrote this code with two goals in mind:
- Demonstrate how to work with the PSX network model.
- Demonstrate how the relatively new asyncio library can be
used to write robust, understandable, and maintainabe network code without
using threads and other artifacts that tend to make things complicated.
I do not claim that this approach is the best or that there are no other
ways, and if you know what you are doing threads certainly are okay, but for
many applications, keeping things asynchronous with an event loop is a very
clean solution. If you are not trying to squeeze the very last bit of
performance out of your multicore CPU, which needs work that is inherently
parallelizable, you don't need threads. When done properly, both methods use
zero CPU if there is no incoming data.
The psx-print-pushover.py file is not part of the tutorial and
an earlier item that performs a useful function. For more info, see the PSX forum
thread about it.
This is not a plain Python tutorial. If you are not sufficiently fluent
in the language yet, I recommend you to use the excellent Python tutorial
referenced above (part of the Python documentation) to figure out what the
various language constructs do. For questions, either search the Web, or
post in the appropriate thread on the Aerowinx
Forum.
What does psx-python-demo.py do?
Not a lot!
This script connects to PSX, and listens to the MCP Altitude Window. It
shows you the value in that window every time it changes, for example when
you turn the MCP Altitude knob.
If the altitude is below 37,000 ft, the Left FMC is selected (that
selector knob is just below the MCP Altitude knob). If the altitude is
37,000 ft or higher, the Right FMC is selected. That's it.
However these minimal things demonstrate how to listen to simulator
variables and how to kick physical controls around. You can easily add new
functions while not touching the networking and database code at all.
class PSX
This structure contains the networking part of the script. It attempts to
maintain a link to PSX all the time and will retry if something went amiss.
When a connection has been established, the received variables are sent to
the database so that they can be processed.
Note that although it is a Class, there is not such a thing as a PSX
Connection Object. I abuse the class as a namespace, it only is an envelope
around a few related methods, but it works fine. Having more than one PSX
connection is just not going to happen anyway.
The two class variables reader and writer
are to store the currently open socket to PSX. They are declared outside the
connect() method because send() also needs
access.
The connect() method is the work horse. It is declared as an
async
co-routine, so it can block itself on an I/O stream if there is nothing
to do, without stopping the whole program dead. This is the core feature of
the asyncio library. The method gets the usual host and port
parameters to know where to find PSX.
The first block is to connect to PSX and cleanly fail that if PSX isn't
ready or for whatever other reason. Note the use of the
asyncio.sleep() which also is a co-routine that, when pausing
this method, does not stop the whole program. That is what the
await keyword does, but it needs an "awaitable" co-routine to
work.
The second block is the connection handler. It loops around for each
received line from PSX. If something seems odd, it ends and then the outer
loop tries to to reconnect. For each received line, the code decides whether
it is a Lexicon update or a normal PSX update, and calls the appropriate
handler. Most of the code is to handle network issues, like usual with
networking programs.
The separate send() method simply sends the given (key,
value) pair to PSX. The only trick it plays, is that if a key is specified
as a Lexicon mnemonic instead of as a meaningless Q-code, it will remap it
to the Q-code. PSX understands and emits only Q-codes.
Note that this whole PSX class knows nearly nothing of what data PSX
exchanges. Most of that is in the territory of the Db
class.
class Db
Each subscribed variable is stored in the database, so that the script
can get its last known value whenever it needs it. Additionally, variables
may have an associated callback function that is called immediately after a
change. This avoids the script having to poll variables all the time.
This class also is not supposed to be instantiated into objects, for the
same reasons as the previous class. There is no use in keeping two parallel
databases or two parallel PSX connections. But the class construct in Python
also gives a nice "capsule" to warp up things that belong together.
A few simple dictionaries keep the relevant data structures.
lexicon holds the translation map between Q-codes and variable
names. variables holds the last known value of the Q-code. And
callbacks contains a list, per Q-code, of all functions that
should be called just after a new value for the Q-code becomes available.
There is no indexing or other performance optimization beyond what Python
internally does with the hash tables. Never add code just because you can.
Wait until you run into performance issues first. Premature optimization
is the root of all evil.
The class methods to straightforward things. You can just read the code
to see what. If you don't grasp a Python construct, look it up in the
excellent Python documentation referenced above. Or ask me.
function McpWdoAlt()
A very simple processing callback. It displays the new value shown in the
MCP Altitude Window, and depending on its value, flips the FMC selector left
or right.
Note that this function is not declared as async . It is a
normal function. Only the PSX class has some async functionality, all the
rest of the program is plain synchronous callable.
function Main()
Python likes it best when the main event loop is set up as a separate
co-routine. This function does just that. When called, it sets up a list of
tasks (in this case, just one) and waits until they all return their final
result. In this program, the only task is not designed to ever return, but a
Ctrl-C keyboard interrupt can cancel it which causes it to return.
Executable part
This is the part of the Python script that actually does things, instead of
defining tooling like the classes and functions/coroutines. It begins with a
simple check for Python 3.8 or better. Then it processes command line
arguments, if any.
Before a PSX connection is opened, the script subscribes to all PSX
variables it is interested in. Some of these are not related to the
simulated aircraft, such as id and version . For the
others, I personally prefer to use their nickname, such as
FmcEicas , instead of their Q-code. The PSX Lexicon hands you the
translation and this demonstration shows you how to work with it. Note that
you need to subscribe to both the input and the output variables to get the
lexicon mapping. The Db functions accept both nicknames and
Q-codes.
Some subscriptions have an associated function that is called when a new
value of that variable has been received. The McpWdoAlt() function
is a good example. For the version key, I used a nameless function
built with the Python lambda feature. A nice thing to study but by
no means important; I could have declared a normal function as well.
Lastly, the script starts the Event Loop and feeds it the background
tasks it wants to run. The program will remain in there until something
blows up or you hit the Ctrl-C key to stop the script.
PSX Support Module
Much of the code in the two toplevel scripts has been isolated in a
Python module, so you can cleanly separate your own code from the module
that you may update once in a while. The structure of this module is largely
the same as of the original monolithic code. I have improved a few sections
because I also still find out better ways to code elements. Often the
functionality is the same, but it reads better.
The psx.py can be used by importing it as usual. The module
will load a class that you can use to connect to your PSX Main Server.
To show you how things are done, and to give a quick self-test and
demonstration, the module also has code at the bottom that will be executed
if you do not import it, but execute it directly as if it were
a Python script (application). Just type: python psx.py and the
demonstration will start. It looks for your PSX on the same machine, if you
run PSX elsewhere you will need to update the IP address. Do this by
adjusting the example code on line 619:
asyncio.run(psx.connect())
becomes
asyncio.run(psx.connect(host="192.168.1.34"))
or whatever your server's IP address is.
Class PSX
Basically the same as in the monolithic script. A few more hooks to
install your own functions, so you can respond to events such as PSX
connecting, pausing, or disconnecting.
By adding the enter and exit "magic methods",
it becomes possible to work with the Python with statement.
This makes it a lot cleaner to assure the connection is properly closed when
the script is ended. You can see this when you Ctrl-C the script: PSX will
cleanly unplug the MCDU head, instead of just leaving the display as it was
when the module ended.
I have included the previous Db class into the
Client class, as nobody would have two databases supporting the
same Client anyway.
Class MCDU
This is new. PSX has good support to "install" new avionics (subsystems)
on the three MCDU heads, but the interface is low-level and can be cleaned
up by specialized code. This class contains such code. It brings PSX up to
nearly the ARINC 739A standard, so it can be used to convert PSX into a MCDU
emulator for various applications. Note that it is not actually using ARINC
429 words to control the MCDUs, though this would be straightforward to
add.
In order to control a MCDU, just as in reality, you must first wire it
in. Each MCDU is wired separately, and each prompt is wired in separately,
too, just as in a real aircraft. You first set up a "head" to define where
your wires go: which MCDU (you have three), which prompt position (you have
several), and which prompt text will appear there. Then, after PSX gets
connected, you "plug in" the head to the aircraft, which will make the
prompt appear on the actual display.
The rest of the MCDU operation is straightforward, and you are invited to
check the example code in the script to see how it works. This example code
doubles as self-test, nearly all available features of the MCDU class and
the PSX class are exercised by it.
Independently of PSX, I am working on a MCDU control library that brings
a host of features to the party. With this one you can develop professional
avionics interfaces that can use PSX, but also other interfaces and
components, including real aircraft. But this takes a while.
Comments? Questions?
Please use this thread on
the PSX Forum for all communications about this web page. You're
welcome!
|