Python Tutorial for Aerowinx PSX

Hoppie Home

File size 5.9 KiB
22-Mar-2020 16:57Z

File size 6.8 KiB
22-Mar-2020 17:54Z

This is a work in progress. For discussion and suggestions, please use the Aerowinx Forum.

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:
  1. Demonstrate how to work with the PSX network model.
  2. 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 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 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.

Comments? Questions?

Please use this thread on the PSX Forum for all communications about this web page. You're welcome!

© 2020 Jeroen Hoppenbrouwers For more information, mail to