Today’s post will take us in a slightly different direction than the last few. Today’s post will be about hooking up some Python code to the X-Plane flight simulator to enable development of an autopilot using PID (proportional-integral-derivative) controllers. I’ve been a fan of flight simulators for quite some time (I distinctly remember getting Microsoft Flight Simulator 98 for my birthday when I was like 8 or 9) but have only recently started working with interfacing them to code. X-Plane is a well-known flight simulator developed by another Austin – Austin Meyer. It is regarded as having one of the best flight models and has tons of options for getting data into/out of the simulator. More than one FAA-certified simulator setups are running X-Plane as the primary driver software.
I got started thinking about writing some code for X-Plane while playing another game, Factorio. I drive a little plane or car in the game to get around my base and I just added a plug-in that “snaps” the vehicle to a heading, which makes it easier to go in straight lines. I thought – “hmm how hard could this be to duplicate in a flight sim?”. So here we are.
This post will get X-Plane hooked up to Python. The real programming will start with the next post.
Download and install X-Plane (I used X-Plane 10 because it uses less resources than X-Plane 11 and we don’t need the graphics/scenery to look super pretty to do coding. It also loads faster.)
Download and install NASA’s XPlaneConnect X-Plane plug-in to X-Plane
Verify the XPlaneConnect plug-in is active in X-Plane
Download sample code from XPlaneConnect’s GitHub page
Run the sample script to verify data is being transmitted from X-Plane via UDP to the XPlaneConnect code
1 – Download and install X-Plane 10 or X-Plane 11
I’ll leave this one up to you. X-Plane 10 is hard to find these days I just discovered. X-Plane 11 is available on Steam for $59.99 as of writing. I just tested and the plug-in/code works fine on X-Plane 11 (but the flight models are definitely different and will need different PID values). My screenshots might jump between the two versions but the content/message will be the same.
2 – Download and install NASA’s XPlaneConnect plug-in
NASA (yes, that NASA, the National Aeronautics and Space Administration) has wrote a bunch of code to interface with X-Plane. They have adapters for C, C++, Java, Matlab, and Python. They work with X-Plane 9, 10, and 11.
Open the .zip and place the contents in the [X-Plane directory]/Resources/plugins folder. There are few other folders already present in this directory. Mine looked like this after adding the XPlaneConnect folder:
3 – Verify XPlaneConnect is active in X-Plane
Now we’ll load up X-Plane and check the plug-ins to verify XPlaneConnect is enabled. Go to the top menu and select Plugins -> Plugin Admin. You should see X-Plane Connect checked in the enabled column:
4 – Download sample code from XPlaneConnect’s GitHub page
5 – Run sample code to verify data is making it from X-Plane to our code
With X-Plane running with a plane on a runway (or anywhere really), go ahead and run monitorExample.py! I will be using Visual Studio Code to program this XPlane Python autopilot stuff so that’s where I’ll run it from.
You will start seeing lines scroll by very fast with 6 pieces of information – latitude, longitude, elevation (in meters), and the control deflections for aileron, elevator, and rudder (normalized from -1 to 1, with 0 being centered). In the below screenshot, we see a lat/lon of 39.915, -105.128, with an elevation of 1719m. First one to tell me in the comments what runway that is wins internet points!
In this post, we have successfully downloaded the XPlaneConnect plug-in, and demonstrated that it can successfully interface with Python code in both X-Plane 10 and X-Plane 11.
Next up is to start controlling the plane with a basic wing leveler. As of writing this post, I have the following completely functional:
Pitch / roll hold at reasonable angles (-25 to 25)
Building off my last NTP post (Microsecond accurate NTP with a Raspberry Pi and PPS GPS), which required a $50-60 GPS device and a Raspberry Pi (also $40+), I have successfully tested something much cheaper, that is good enough, especially for initial PPS synchronization. Good enough, in this case, is defined as +/- 10 milliseconds, which can easily be achieved using a basic USB GPS device: GT-U7. Read on for instructions on how to set up the USB GPS as a Stratum 1 NTP time server.
How accurate of time do you really need? The last post showed how to get all devices on a local area network (LAN) within 0.1 milliseconds of “real” time. Do you need you equipment to be that accurate to official atomic clock time (12:03:05.0001)? Didn’t think so. Do you care if every device is on the correct second compared to official/accurate time (12:03:05)? That’s a lot more reasonable. Using a u-blox USB GPS can get you to 0.01 seconds of official. The best part about this? The required USB GPS units are almost always less than $15 and you don’t need a Raspberry Pi.
This post will show how to add a u-blox USB GPS module to NTP as a driver or chrony (timekeeping daemon) as a reference clock (using GPSd shared memory for both) and verify the accuracy is within +/- 10 milliseconds.
USB u-blox GPS (VK-172 or GT-U7), GT-U7 preferred because it has a micro-USB plug to connect to your computer. It is important to note that both of these are u-blox modules, which has a binary data format as well as a high default baudrate (57600). These two properties allow for quick transmission of each GPS message from GPS to computer.
1 – Update your host machine and install packages
This tutorial is for Linux. I use Ubuntu so we utilize Aptitude (apt) for package management:
In /etc/default/gpsd, change the settings to the following:
# Start the gpsd daemon automatically at boot time
# Use USB hotplugging to add new USB devices automatically to the daemon
# Devices gpsd should collect to at boot time.
# this could also be /dev/ttyUSB0, it is ACM0 on raspberry pi
# -n means start listening to GPS data without a specific listener
Reboot with sudo reboot.
3a – Chrony configuration (if using NTPd, go to 3b)
I took the default configuration, added my 10.98 servers, and more importantly, added a reference clock (refclock). Link to chrony documentation here. Arguments/parameters of this configuration file:
iburst means send a bunch of synchronization packets upon service start so accurate time can be determined much faster (usually a couple seconds)
maxpoll (and minpoll, which isn’t used in this config) is how many seconds to wait between polls, defined by 2^x where x is the number in the config. maxpoll 6 means don’t wait more than 2^6=64 seconds between polls
refclock is reference clock, and is the USB GPS source we are adding
‘SHM 0’ means shared memory reference 0, which means it is checking with GPSd using shared memory to see what time the GPS is reporting
‘refid NMEA’ means name this reference ‘NMEA’
‘offset 0.000’ means don’t offset this clock source at all. We will change this later
‘precision 1e-3’ means this reference is only accurate to 1e-3 (0.001) seconds, or 1 millisecond
‘poll 3’ means poll this reference every 2^3 = 8 seconds
‘noselect’ means don’t actually use this clock as a source. We will be measuring the delta to other known times to set the offset and make the source selectable.
[email protected]:~ $ sudo cat /etc/chrony/chrony.conf
# Welcome to the chrony configuration file. See chrony.conf(5) for more
# information about usuable directives.
#pool 2.debian.pool.ntp.org iburst
server 10.98.1.1 iburst maxpoll 6
server 10.98.1.198 iburst maxpoll 6
server 10.98.1.15 iburst
refclock SHM 0 refid NMEA offset 0.000 precision 1e-3 poll 3 noselect
Restart chrony with sudo systemctl restart chrony.
3b – NTP config
Similar to the chrony config, we need to add a reference clock (called a driver in NTP). For NTP, drivers are “servers” that start with an address of 127.127. The next two octets tell what kind of driver it is. The .28 driver is the shared memory driver, same theory as for chrony. For a full list of drivers, see the official NTP docs. To break down the server:
‘server 127.127.28.0’ means use the .28 (SHM) driver
minpoll 4 maxpoll 4 means poll every 2^4=16 seconds
noselect means don’t use this for time. Similar to chrony, we will be measuring the offset to determine this value.
‘fudge 127.127.28.0’ means we are going to change some properties of the listed driver
‘time1 0.000’is the time offset calibration factor, in seconds
‘stratum 2’ means list this source as a stratum 2 source (has to do with how close the source is to “true” time), listing it as 2 means other, higher stratum sources will be selected before this one will (assuming equal time quality)
Running gpsmon shows us general information about the GPS, including time offset. The output looks like the below screenshot. Of importance is the satellite count (on right, more is better, >5 is good enough for time), HDOP (horizontal dilution of precision) is a measure of how well the satellites can determine your position (lower is better, <2 works for basically all navigation purposes), and TOFF (time offset).
In this screenshot the TOFF is 0.081862027, which is 81.8 milliseconds off the host computer’s time. Watch this for a bit – it should hover pretty close to a certain value +/- 10ms. In my case, I’ve noticed that if there are 10 or less satellites locked on, it is around 77ms. If there are 11 or more, it is around 91ms (presumably due to more satellite information that needs to be transmitted).
5 – record statistics for a data-driven offset
If you are looking for a better offset value to put in the configuration file, we can turn on logging from either chrony or NTPd to record source information.
Edit /etc/chrony/chrony.conf and uncomment the line for which kinds of logging to turn on:
# Uncomment the following line to turn logging on.
log tracking measurements statistics
Then restart chrony (sudo systemctl restart chrony) and logs will start writing to /var/log/chrony (this location is defined a couple lines below the log line in chrony.conf):
[email protected]:~ $ ls /var/log/chrony
measurements.log statistics.log tracking.log
For NTPd (be sure to restart it after making any configuration changes):
[email protected]070 ~ % cat /etc/ntp.conf
# Enable this if you want statistics to be logged.
statistics loopstats peerstats clockstats
filegen loopstats file loopstats type day enable
filegen peerstats file peerstats type day enable
filegen clockstats file clockstats type day enable
Wait a few minutes for some data to record (chrony synchronizes pretty quick compared to NTPd) and check the statistics file, filtered to our NMEA refid:
cat /var/log/chrony/statistics.log | grep NMEA
This spits out the lines that have NMEA present (the ones of interest for our USB GPS). To include the headers to show what each column is we can run
cat /var/log/chrony/statistics.log | head -2; cat /var/log/chrony/statistics.log | grep NMEA
# ntp, there is no header info so we can omit that part of the command
cat /var/log/peerstats | grep 127.127.28.0
NTP stats don’t include header information. The column of interest is the one after the 9014 column. The columns are day, seconds past midnight, source, something, estimated offset, something, something, something. We can see the offset for this VK-172 USB GPS is somewhere around 76-77 milliseconds (0.076-0.077 seconds), which we can put in place of the 0.000 for the .28 driver for NTP and remove noselect.
So now we have some data showing the statistics of our NMEA USB GPS NTP source. We can copy and paste this into Excel, run data to columns, and graph the result and/or get the average to set the offset.
This graph is certainly suspicious (sine wave pattern and such) and if I wasn’t writing this blog post, I’d let data collect overnight to determine an offset. Since time is always of the essence, I will just take the average of the ‘est offset’ column (E), which is 7.64E-2, or 0.0763 seconds. Let’s pop this into the chrony.conf file and remove noselect:
Restart chrony again for the config file changes to take effect – sudo systemctl restart chrony.
6 – watch ‘chrony sources’ or ‘ntpq -pn’ to see if the USB GPS gets selected as the main time source
If you aren’t aware, Ubuntu/Debian/most Linux includes a utility to rerun a command every x seconds called watch. We can use this to watch chrony to see how it is interpreting each time source every 1 second:
# for chrony
watch -n 1 chronyc sources
In the above screenshot, we can see that chrony actually has the NMEA source selected as the primary source (denoted with the *). It has the Raspberry Pi PPS NTP GPS ready to takeover as the new primary (denoted with the +). All of the sources match quite closely (from +4749us to – 505us is around 5.2 milliseconds). The source “offset” is in the square brackets ([ and ]).
# for ntp
watch -n 1 ntpq -pn
7- is +/- five millseconds good enough?
For 99% of use cases, yes. You can stop here and your home network will be plenty accurate. If you want additional accuracy, you are in luck. This GPS module also outputs a PPS (pulse per second) signal! We can use this to get within 0.05 millseconds (0.00005 seconds) from official/atomic clock time.
In this post, we got a u-blox USB GPS set up and added it as a reference clock (refclock) to chrony and demonstrated it is clearly within 10 millisecond of official GPS time.
You could write a script to do all this for you! I should probably try this myself…
In the next post, we can add PPS signals from the GPS module to increase our time accuracy by 1000x (into the microsecond range).
A note on why having faster message transmission is better for timing
My current PPS NTP server uses chrony with NMEA messages transmitted over serial and the PPS signal fed into a GPIO pin. GPSd as a rule does minimum configuration of GPS devices. It typically defaults to 9600 baud for serial devices. A typical GPS message looks like this:
$GPGGA, 161229.487, 3723.2475, N, 12158.3416, W, 1, 07, 1.0, 9.0, M, , , , 0000*18
That message is 83 bytes long. At 9600 baud (9600 bits per second), that message takes 69.1 milliseconds to transmit. Each character/byte takes 0.833 milliseconds to transmit. That means that as the message length varies, the jitter will increase. GPS messages do vary in length, sometimes significantly, depending on what is being sent (i.e. the satellite information, $GPGSV sentences, is only transmitted every 5-10 seconds).
I opened gpsmon to get a sample of sentences – I did not notice this until now but it shows how many bytes each sentence is at the front of the sentence:
These sentences range from 83 bytes to 35 bytes, a variation of (83 bytes -35 bytes)*0.833 milliseconds per byte = 39.984 milliseconds.
Compare to the u-blox binary UBX messages which seem to always be 60 bytes and transmitted at 57600 baud, which is 8.33 milliseconds to transmit the entire message.
The variance (jitter) is thus much lower and can be much more accurate as a NTP source. GPSd has no problem leaving u-blox modules at 57600 baud. This is why the USB GPS modules perform much more accurate for timekeeping than NMEA-based devices when using GPSd.
For basically every GPS module/chipset, it is possible to send it commands to enable/disable sentences (as well as increase the serial baud rate). In an ideal world for timekeeping, GPSd would disable every sentence except for time ($GPZDA), and bump up the baud rate to the highest supported level (115200, 230400, etc.). Unfortunately for us, GPSd’s default behavior is to just work with every GPS, which essentially means no configuring the GPS device.
This WordPress blog is decently protected from bots/hackers (read more at Securing this WordPress blog from evil hackers!) but I still get a ton of attempts on the site. Wordfence can block requests at the application layer but as I grow in traffic, I want to make sure CPU cycles aren’t wasted. Thus, I want to block some of these bots/hackers from even connecting to the server. I use Ubuntu, and UFW (Uncomplicated FireWall) is included. It’s pretty simple so I’ve stuck with it.
Blocking IPv4 is easy:
sudo ufw insert 1 deny from 188.8.131.52 comment "repeated unwanted hits on sdrforums.com"
The command broken down:
sudo – run as root since firewall modification requires root access
ufw – run the uncomplicated firewall program
insert – add a rule
1 – insert at the top of the rule list (firewalls evaluate rules from the top down – putting a deny after an allow would mean the traffic wouldn’t be blocked)
deny – deny the request
from – from the following IP
184.108.40.206 – IP address
comment – so you can leave a comment to remind yourself why the rule is in place (“unwanted hits on site.com”, “change request #123456”, “incident remedy #44444”, etc.)
Blocking IPv6 with UFW
So I tried the same command format with an IPv6 address and got an error message – “ERROR: Invalid position ‘1’”. I’ve never got that message before. Also, I do realize I need to widen this IPv6 subnet and block a much larger range of IPs but that’s a topic for a different day.
sudo ufw insert 1 deny from 2400:adc5:11f:5600:e468:9464:e881:b1c0 comment "repeated unwanted hits on sdrforums.com"
ERROR: Invalid position '1'
My rule list at the time looked like this:
[email protected]:~$ sudo ufw status numbered
To Action From
-- ------ ----
[ 1] Anywhere DENY IN 220.127.116.11 # repeated unwanted hits on sdrforums.com
[ 2] Anywhere DENY IN 18.104.22.168 # repeated unwanted hits on sdrforums.com
[ 3] Anywhere DENY IN 22.214.171.124 # excessive hits to wp-login and xmlrpc
[ 4] 22/tcp ALLOW IN Anywhere
[ 5] 80/tcp ALLOW IN Anywhere
[ 6] 443/tcp ALLOW IN Anywhere
[ 7] 22/tcp (v6) ALLOW IN Anywhere (v6)
[ 8] 80/tcp (v6) ALLOW IN Anywhere (v6)
[ 9] 443/tcp (v6) ALLOW IN Anywhere (v6)
Pretty easy to understand what’s going on here. I have a few IPv4 addresses blocked, but nothing specific to IPv6. A bit of searching later and I learned that the first IPv6 rule needs to come after the last IPv4 rule. So in this case I needed to add the rule to position #7, since that is where the first IPv6 rule current is located:
[email protected]:~$ sudo ufw insert 7 deny from 2400:adc5:11f:5600:e468:9464:e881:b1c0 comment "repeated unwanted hits on
Rule inserted (v6)
SDR stands for software defined radio. It is a term used to describe devices that can receive radio frequency signals over a wide range of frequencies. These devices most commonly interface with USB drives of computers / Raspberry Pi / similar. There are standalone-devices but they often cost quite a bit more than the USB devices. The best part about the base-level SDRs? They’re less than $50. I have a couple that I paid $18 for.
So you’re here on the “Getting Started with SDR” post. In this SDR tutorial post we’ll get a SDR plugged into your Windows computer, install drivers, and start sniffing for signals. We’ll try FM radio first since they have very powerful transmitters and there is almost certainly one you can receive at your hose, followed by checking for those wireless temperature sensors, and then some aircraft position signals.
What does a SDR look like? Below is a image with 3 I have around the house that I snagged for a picture (USB plug for size reference). Below the picture is the table of contents for the post.
SDR Tutorial Contents
Purchasing a suitable SDR
Plugging it into a Windows computer and installing drivers
Installing SDR# (a program to monitor any and all frequencies)
Listening to a FM radio station
Installing rtl_433 and listening for wireless temperature sensors
Installing dump1090 and listening for aircraft position signals
1 – Purchasing a suitable SDR
To get started, you’ll obviously need a RTL-SDR. I recommend the NooElec devices, specifically the NooElec NESDR Smart v4 bundle. NooElec devices have lasted the longest out of the 8 SDRs I’ve purchased and have been very useful for me. The link below shows the current price of the newest version (V4) and includes 3 different antenna (433 MHz for sensors, 1090 MHz for ADS-B aircraft signals, and an adjustable antenna) which are useful for the rest of the tutorial:
With a SDR in hand, let’s get started!
2 – Installing drivers and plugging in the RTL-SDR
I use Windows 10 for this SDR tutorial, but I use Linux for the long-term monitoring I have in place around the house. Linux doesn’t ever automatically reboot for updates, and is generally much more stable (and doesn’t need a license). Windows is easy to get started so we’ll use it.
The instructions for installation are a bit long so I’ll just leave a link to the official source where they will always have the most up to date drivers and such – https://www.rtl-sdr.com/rtl-sdr-quick-start-guide/. I will re-write these at some point (and also make a video) but I’ll just leave the link for now.
3 – SDRSharp installation
If you followed the instructions in the link above, you should have a file called SDRSharp.exe in the folder you used:
This means it is already “installed” since it is just a simple executable file. No need to click next, next, next finish to install.
4 – Listening to a FM radio station with your RTL-SDR
Go ahead and double-click on SDRSharp.exe and launch it! First thing you need to do is select the RTL-SDR USB option from the sources drop down menu:
Next up, we need to crank the gain to get useful signal out of the SDR. Click the gear icon, make sure the RTL2832U device is selected, then set the RF gain slider to around 40dB, then click close:
Now we are ready to listen to FM radio!
Ensure the WFM radio button is selected in the signal type, set the zoom slider all the way down, set the step to 100 kHz, then click the frequency numbers up/down to pick a known FM station. This should get everything ready to go:
With all that setup out of the way, click the play button and watch the signals start streaming in! Assuming your speakers are set to a decent volume, you’ll hear the radio from your computer! From my 2nd story bedroom near Broomfield, CO, I can easily get 98.5 MHz and 99.5 MHz (and many others). You can also check the “FM Stereo” checkbox on the left side if you know you’ll be receiving FM stereo. Make sure you uncheck it if you start looking at other things:
Here is a picture showing my setup for writing this blog – a simple NooElec Nano SDR I bought in 2016, a little bit of cable, and a 1090 MHz antenna (which is clearly not ideal for FM radio frequencies, but radio is so powerful it doesn’t really matter). Also say hi to Fluffy the cat:
5 – Installing rtl_433 and listening for wireless temperature/humidity sensors
Ok now we’re in the rtl_433 directory so we can run commands now. The most basic command for this program is to run it and only specify the gain. We used 40 dB for the FM radio so let’s use 40 again by specifying it with the -g option (you can view all commands by running rtl_433 -h):
rtl_433 -g 40
The output will show the following (ending in “Tuned to 433.92 MHz”) if all went well:
Let it run for a couple minutes. You might see some thermometers and other such devices! Here is what my output looks like after about 60 seconds:
If you look closely, you can see there are three separate devices broadcasting on 433 MHz that I can pick up with my (still not desirable) 1090 MHz antenna! One is sitting in my garage, another is next to me in the bedroom, and I don’t actually know where the 3rd is or who owns it.
With the same, tiny USB RTL-SDR, we have picked up FM radio as well as temperature/humidity readings from three separate sensors!
6 – Installing dump1090 and listening for aircraft position (ADS-B) signals
Last up for this RTL-SDR tutorial is installing dump1090 and checking to see if we can pick up any aircraft signals.
Dump1090 is a utility written many years ago that decodes aircraft ADS-B position signals. Some brave souls ported it for use in Windows a while ago. Using the directions and links from https://sonicgoose.com/using-dump1090-in-windows/ I have distilled it down for you. Download the dump1090 windows package that I’ve rehosted here –
And then extract it to your desktop in a folder called dump1090.
There is a .bat file (batch file) in the folder, double-click it to run it:
You will be presented with a screen that updates in realtime as signals come in. You are now receiving ADS-B signals with the same device that could listen to FM radio, and also temperature/humidity sensors around the house!
All this from a 1090 MHz antenna (which is actually finally the right frequency for the application at hand). Fluffy is still supervising:
In this SDR tutorial, we have purchased a RTL-SDR, installed the drivers, plugged it in, listened to FM radio, checked for wireless temperature/humidity sensors and found 3, and listened for aircraft ADS-B signals (and found 15 aircraft broadcasting in the last screenshot). This is an addictive hobby. With the right antenna, you can hear people speaking and morse code coding from across the world. In my next post I’ll show how to implement some of these into automated programs to take the data and input it into Home Assistant and other databases.
One last thing – receiving RF signals is 98% about the antenna and 2% about what you’re receiving the signals with! The base antennas that come with RTL-SDRs are good for the basics but if you really want to get into receiving interesting/distant signals, be prepared to spend 2-3x the cost of a SDR on a single antenna!
If you’re looking for more SDR discussion, head on over to SDRForums.com.
Hope you learned something and enjoyed this tutorial!
In my quest for doing more nerdy things, I wanted to set up a forum. I haven’t come across any good forums for SDRs so I decided to make my own. I took some time to decide on SDR forum software (MyBB), installed it (on the same host as this blog), and linked it to a domain (sdrforums.com). I’ll blog about the setup process in a later blog – it wasn’t as straight-forward as I thought it would be (especially hooking it up to an email service).
The new forum is called SDRForums.com! It is a place to discuss all things SDR (Software Defined Radio). The site is quick, has the classic forum look, and is ready to serve many users! I still need to do some customizing though, like for the logo and things like that.
If you’re interested in SDR discussions, head on over, register an account, and make a post!