Improved ADS-B Toolchain

After my recent post I did not stop tinkering and exploring. To my own surprise it didn’t take long to come across significant improvements to my toolchain.

Someone contacted me on mastodon shortly after and said that a friend of his had also recently gotten into SDRs and ADS-B and decided to write his on demodulator called stream1090. While this is actively in development and still early stages, this part from the README resonated with me.

“Most implementations look for the so-called preamble (a sequence of pulses anounncing a message). Stream1090 skips this step and maintains directly a set of shift registers. Based on the CRC sum and other criteria, messages are being identified. The hope is that in high traffic situations, a higher overall message rate can be achieved compared to a preamble based approach.”

Being only a demodulator means that the emitted data needs further processing. Additionally, this tool does not directly talk to your SDR, which makes the resulting pipeline seem a bit more complex at first — but it’s not as bad as it looks.

In my last post the toolchain consisted of two tools

  • readsb (read from SDR, demodulate, decode, write stats)
  • tar1090 (read stats, render map)

With the new toolchain I’m streaming demodulated data into readsb. For this to work, the readsb command has two additional flags for specifying the port on which to stream data into and another port for some graphs (more on that later).

The full command looks like this now:

./readsb --net-only \
  --net-ri-port 30001 \
    --net-bo-port 30005 \
    --interactive \
    --lat 52.5200 \
    --lon 13.4050 \
    --write-json ~/src/adsb/json \
    --write-json-every 1 \
    --db-file "$HOME/src/readsb/aircraft.csv.gz"

stream1090

Now it’s time to stream some data into readsb. As with most of these tools, none of them consider macOS as a target platform but luckily for me, I could compile all of them successfully albeit with some tinkering. In this case compilation was pretty effortless and I just followed the instructions from the README.

After that I started readsb with the command above and then used this command to get the data from the SDR, pipe it into stream1090 and then into readsb via socat.

rtl_sdr -g 49.6 -f 1090000000 -s 2400000 - | ./build/stream1090 | socat -u - TCP4:localhost:30001

Improved rendering and graph setup

The rendering setup thus far isn’t bad. Essentially I just started a python web server for tar1090 to render a nice map. However, I wanted more longterm statistics of my experiments and searched for some kind of graphing solution.

Maybe you’ve already guessed it but of course there is a tool for it and the name is of course graph1090 (https://github.com/wiedehopf/graphs1090).

Compared to the other tools so far, this one is not as nice to install as it as a bunch of dependencies that I don’t want to install on my main workstation. Among them are classics like rrdtool, gnuplot and coreutils – with the first two having many more dependencies.

Docker / Apple Container / OCI solution

I was wondering if there might be a Docker image that I could use instead and if there wasn’t I’d rather make one myself to contain this zoo of dependencies.

Again luckily for me, there are (at least) two suitable docker images.

Both of these bundle a lot of ADS-B processing tools at the price of shoehorning all those config parameters of the various tools into the Docker domain.

I’ve started with the first image which I got running using Apple Containers

container run -d --name tar1090 \
  --publish 8080:80 \
  --env TZ=Europe/Berlin \
  --env BEASTHOST=10.0.0.80 \
  --env BEASTPORT=30005 \
  --env LAT=52.5200 \
  --env LONG=13.4050 \
  --volume ~/src/adsb/graphs:/var/lib/collectd \
  ghcr.io/sdr-enthusiasts/docker-tar1090:latest

With the the two previous commands already running I could then point my browser to localhost:8080 to see my familiar tar1090 map.

Screenshot of tar1090

Additionally I got a barely visible link to some stats which you can make permanent between machine / container restarts with the --volume flag.

Screenshot of ADS-B graphs

While the second image (ultrafeeder) looks even more complete and feature rich, I’ll stick with the first image for the time being.

Further enhancements

As you can see, my reception range has increased quite a bit. I noticed a slight improvement when switching to stream1090 and then I moved my little dipole telescope kit antenna as close to the window as possible.

Later this week I will receive a combined Low Noise Amplifier (LNA) and Filter specifically for ADS-B. I will add another post if that yields signficant reception improvements.

My minimal ADS-B Tracking Setup

For a while I was somewhat interested in playing around with a Software Defined Radio (SDR). Recently I bought myself an entry level SDR and started exploring.

One of the first things I was interested in was receiving and tracking ADS-B data. This is a specific radio transmission used by aircraft to broadcast positional data, among other things. This is what sites like flightradar24 are based on. Most information that is displayed there is based on ADS-B data received by probably thousands of individual receivers around the globe.

The SDR I bought was the ubiquitous RTL-SDR Blog v4 with a basic antenna kit which cost me 99€.

This is where I bought it: https://www.wimo.com/de/rtl-sdr-v4-kit

First steps

I am a total beginner when it comes to radio and specifically software defined radios apart from very general knowledge of how electro magnetic signals work. Therefore I had to do quite a lot of research before I received and decoded the first actual ADS-B message.

Question number one: What kind of antenna is needed to receive ADS-B?

The frequency that is being used can be easily looked up. It’s on 1090Mhz. But what is somewhat harder to find out is the optimal length and orientation of the antenna.

The kit which I bought contains a simple dipole antenna base with two different pairs of metal telescopic antennas. After a bit of googling I found out that there is a formula for calculating the optimal length of a dipole antenna for a given center frequency. Then I looked up a basic Dipole Calculator like this one: https://www.wireantennas.co.uk/dipole-calculator

I ended up with 13.086cm of total length for the dipole. Then I screwed on the two short telescopic antennas to the dipole base, connected everything and started SDR++ to have a look.

Unfortunately there wasn’t much to see but I also recognized the many options to configure your SDR in the software and that I barely knew what any of them meant or related to. So the search continued for “ads-b sdr settings” and the like. Eventually I found some youtube videos that had the crucial information:

ADS-B is vertically polarized which means that the antenna should be oriented vertically and switching SDR++ to “RAW” would prevent it from trying to “interpret” the signal since it’s digital in nature anyway.

(Bonus Tip: Also try both possible vertical orientations since one arm of the dipole is connected to ground / the shield and the other arm to the conductor – some people claim that the orientation can make a difference but I can’t neither confirm nor deny from my limited tests)

Unsure of whether this would work from within my apartment in the middle of the city, I started up SDR++, tuned it to 1090Mhz with autogain enabled and the antenna mounted to a photo tripod, I moved around to see if I could catch any signals. Eventually I found a spot where these patterns appeared.

SDR++ Screenshot for receiving ADS-B Data

Then I sat down the tripod and watched the signal come in.

How to decode and visualize the data?

Now that I was somewhat sure that I actually receive ADS-B data, I wondered how I could best decode and visualize it, ideally on my mac without much hassle. If you’re on Linux most of the steps are much simpler and sometimes even available as package.

I’ll spare you the details of my research which lasted several days and took me down various rabbit holes. At first I found a tool called dump1090 which you can even install via homebrew. There are many forks and permutations of this tool and long story short I’m currently using a toolchain which is very similar to websites like https://adsb.lol

The toolchain essentially consists of two tools

  • readsb (another fairly active dump1090 mutation)
  • tar1090 (the tool doing the visualization)

Building this on macOS was not super straight forward but with a little bit of LLM help decoding the compiler errors I got both to work.

For readsb I had to edit the Makefile and remove all mentions of Werror which didn’t work for some reason.

git clone https://github.com/wiedehopf/readsb.git
cd readsb
perl -pi -e 's/(^|\s)-Werror(\s|$)/ /g' Makefile
make RTLSDR=yes CPPFLAGS="-I/opt/homebrew/include" LDFLAGS="-L/opt/homebrew/lib"

That compiled successfully on my M4 mac running macOS 26.2 (Tahoe). You might need to install some dependencies via homebrew like librtlsdr. Check the project repo for the required packages.

After the compile worked, I could start readsb like this

mkdir -p ~/src/adsb/json

[~/src/readsb:dev*] ./readsb \
  --device-type rtlsdr \
  --lat 52.5200 --lon 13.4050 \
  --write-json ~/src/adsb/json \
  --write-json-every 1 \
  --db-file "$HOME/src/readsb/aircraft.csv.gz" \ (more on this flag in a bit)
  --net

Now you should see output in the terminal as well as in the json directory. All we need to do now is to render it with the tar1090 tool.

git clone https://github.com/wiedehopf/tar1090.git
cd tar1090
ln -s ~/src/adsb/json data 
python3 -m http.server 8080

This should allow you to open your browser and point it to localhost on port 8080. You should see something like this.

Screenshot of tar1090

This screenshot was made after leaving both tools running for a while and receiving a bunch of ADS-B data. The blue outline indicates from where I was able to receive data by correlating the messages from the aircrafts with their reported longitude and latitude.

Now the --db-file flag for the readsb tool allows you to match airplane registration codes with airplane models so if you want to see the airplane type you need to download the most recent db file from here:

https://github.com/wiedehopf/tar1090-db/raw/refs/heads/csv/aircraft.csv.gz

and set the --db-file flag accordingly.

Lastly you can edit the html/config.js file in the tar1090 folder and uncomment/set these 3 options:

// get flight route from routeApi service default setting (toggle via settings checkbox)
useRouteAPI = true;
// configure route display, possible values: iata, icao, city (can use multiple like this: iata+city)
routeDisplay = 'iata';
// which routeApi service to use
// routeApiUrl = "https://adsb.im/api/0/routeset";
routeApiUrl = "https://api.adsb.lol/api/0/routeset";
// routeApiUrl = ""; // to disable route API so it can't be enabled by a website visitor

Note that you might get API rate limited if you live in a busy area with many flights that need to be resolved for their route data.

But that is essentially it. You can see that from within my apartment I’m able to see a bunch of flights with no clear view to the horizon or the sky. Even some aircraft which were north of me. My apartment faces south and so is my antenna but I suspect the houses across the street might reflect some of the signals from the north back to me.

I also had the antenna out once and I could receive signals from A LOT further but for now I’m happy. I might try different antennas or maybe a filter + low noise amplifier specifically tuned for ADS-B but that is not needed to get going.

Put any questions or suggestions in the comments below. I hope this writeup saved some people with a similar beginner knowledge a lot of time.

Mobile Podcasting Setup 2025

I’ll be escaping the grey, dark, cold and wet Berlin for a month. Last year I did the same and I wanted to be able to record a Glitterbrains podcast , record guitar and actually take guitar lessons remotely.

Back then I brought my UA Apollo Twin with me so I could do some flexible audio routing including the fabled mix-minus (n-1) setup and also a Beyerdynamic headset.

Last year I’ve sold my Universal Audio interfaces and switched to an RME interface instead which is far superior when it comes to routing flexibility and software support.

So for this year’s workation, I needed a new mobile audio interface that allowed for flexible routing and so I bought the RME Babyface Pro FS. The investment hurt a little but it’s just so convenient to stay in the same eco system, carry over my TotalMix settings and rely on the great RME engineering. (I also think there is hardly a better audio interface for Podcasting / Streaming than the RME ones)

So without further ado, here is my mobile podcasting, guitar recording and guitar lesson setup:

Mobile Podcasting Equipment

It consists of:

(^ the above are affiliate links to Thomann)

I could definitely save some weight and space by using a headset again but there isn’t currently a model that I feel like investing money in. Therefore I went with a slightly more involved but also more flexible setup. Looking forward to try it out.

Tuning rspamd

For many years I’m running my own mailserver based on postfix and dovecot. To combat spam I’ve used spamassassin like everybody else back in the day but I was never quite satisfied with it. It came from a different era and as the spammers got more sophisticated and billions of people put poorly maintained and therefore hackable computers on the internet, our trusty old friend spamassassin wasn’t keeping up. 

Then in 2013 a new contender entered the scene, rspamd. I remember discovering it, probably a few moons after its initial release and feeling quite excited. It was not written in Perl but in C, promising much better performance and offering a ton of modern features to combat spam.

When I first tried it, its default config was almost enough to get rid of most of the spam that I was struggling to filter with spamassassin but again, over the years as the spammers got more sophisticated, more and more spam was reaching my inbox again which is why I spent a weekend recently to try and figure out what I can do to improve the situation.

The first thing that became obvious to me was that the configuration options and format of certain modules has changed and that certain modules were just not working or even enabled in the first place. 

But that was just the beginning of renovating my rspamd config. So here are a few suggestions for you if you have too much spam in your inbox. I will assume that you are familiar with common email and spamfilter related terms like greylisting and the principles behind it.

Check your config

Suggestion number one is pretty straight forward. Check your active configuration! You can do this by running 

rspamadm configdump

or

rspamadm configdump <module name>

Check if the modules and values are as you expect them to be. Rspamd has a hierarchical config overloading structure and if not fully understood it is easy to believe that what you’ve configured in the local.d folder is actually what is active but I’ve realized that a few of these did not work as expected due to the before mentioned changes in the configuration. 

Deal with the repetitive spam themes first

In my case, I’ve received a lot of similar looking spam. All german speakers probably have seen their fair share of spam mails with a subject like “Apotheke / Apo-theke / A-potheke”. There are many more “common” spam themes and topics and this is what I’ve tackled first because these categories of repetitive spam were very unlikely to produce false positives if I just blacklisted them. 

But if you’re unsure whether this is the right approach on a multiuser setup with varying interests then you can fall back to greylisting. To set this up you will need to edit local.d/multimap.conf and maybe take a look at the corresponding documentation: https://rspamd.com/doc/modules/multimap.html

I’d say this page is one of the most important pieces of documentation to leverage rspamd’s potential.

Subject Blocklist

The first thing in my multimap.conf file is the following block:

BAD_SUBJECT_BL {
  type = "header";
  header = "subject";
  regexp = true;
  map = "$LOCAL_CONFDIR/local.d/local_bl_subject_map.inc";
  description = "Blacklist for common spam subjects";
  score = 10; 
}

The content of that local_bl_subject_map.inc file is as follows:

/\bpsoriasis\b/i
/\bprostatitis\b/i
/\bderila\b/i
/\betf\b/i
/\bbitcoin\b/i
/\breich\b/i
/\bgeld\b/i
/\bki\b/i
/\baktien\b/i
/\bmakita\b/i
/\blotto|lottery\b/i
/\bmubi\b/i
/\bauto\b/i
/\bantihaftbeschichtung\b/i
/.*r[:.-]*?e[:.-]*?z[:.-]*?e[:.-]*?p[:.-]*?t[:.-]*?f[:.-]*?r[:.-]*?e[:.-]*?i/i
/\br[-_]?e[-_]?zept[-_]?frei\b/i
/zeptfrei/i
/\beinkommen\b/i
/\bnubuu\b/i
/\bnuubu\b/i
/\bentgiftungsprogramm\b/i
/\bgelenkschmerzen\b/i
/\bmädchen\b/i
/\bsprachübersetzer\b/i
/\bstabilisierung.+blutdrucks\b/i
/\bmüheloses.+reinigen\b/i
/\bpapillome\b/i
/\bküchenmesser\b/i
/\brendite\b/i
/\bgewichtsverlust\b/i
/\bpreissturz\b/i
/\bchance.+kostenlos\b/i
/\bhamorrhoiden\b/i
/\bhörvermögens\b/i
/\bmuama\b/i
/\bryoko\b/i
/\bbambusseide\b/i
/\bluxusseide\b/i
/\bHondrostrong\b/i
/\btabletten.+apotheke\b/i
/\bEinlegesohlen\b/i
/\bEinlegesohlen\b/i
/\btest\syour\siq\snow\b/i
/\bzukunft.+sauberkeit\b/i
/\bcbd\b/i
/\bharninkontinenz\b/i
/\bpillen\b/i
/\btabletten\b/i

This might seem surprisingly short but this list got rid of the majority of spam mails reaching my inbox. It’s dull, it’s simple but quite effective. Very rarely I have to add things to it these days and it especially effective for those mails that don’t have a lot of suspicious content and fail other spam identification methods.

Again, if you’re uncomfortable to use it as a block / blacklist you can either lower the associated score to be below your global spam threshold or you can convert this map into a prefilter and send the matching mails into greylisting which also gets rid of 95-99% of spam mails.

TLD Blocklist

Speaking of prefilters and greylisting, let’s talk about my most crude blocklist where I apply special treatment on mails coming from certain top level domains. Here is the corresponding entry in local.d/multimap.conf:

SENDER_TLD_FROM {
  type = "from";
  filter = 'email:domain:tld';
  prefilter = true;
  map = "$LOCAL_CONFDIR/local.d/local_bl_tld_from.map.inc";
  regexp = true;
  description = "Local tld from blacklist";
  action = "greylist";
}

And here is the list of “blocked” top level domains:

[.]tr$
[.]su$
[.]mom$
[.]mg$
[.]com\.py$
[.]af$
[.]ng$
[.]ro$
[.]ar$
[.]pro$

For whatever reason, a disproportionate amount of spam mails is coming from those top level domains. Equally for me personally, there is very little chance of false positives but since this is even cruder than the subject based blocking, I changed this to a prefilter which means that this is evaluated before all other checks. I’ve set the action to greylist which basically sends matching mails directly into greylisting and that does the job very well. In case a “good” mail is coming from those top level domains, it should make it through the greylisting and all other modules.

Other Blocklists

I do have a few more blocklists for display names, domains and names (the part of an email address before the @) but they are quite short. For example I get a lot of spam mails from email addresses starting with “firewall@” so again I take care of those. 

The multimap blocks for those look like this: 

SENDER_FROM {
  type = "header";
  header = "from";
  filter = 'email:domain';
  map = "$LOCAL_CONFDIR/local.d/local_bl_from.map.inc";
  description = "Local from blacklist";
  score = 7;
}

SENDER_USER_FROM {
  type = "header";
  header = "from";
  filter = 'email:user';
  map = "$LOCAL_CONFDIR/local.d/local_bl_user_from.map.inc";
  description = "Local user from blacklist";
  score = 7;
}

SENDER_USER_DISPLAY_FROM {
  type = "header";
  header = "from";
  filter = 'email:name';
  map = "$LOCAL_CONFDIR/local.d/local_bl_from_display.map.inc";
  description = "Local user from display name blacklist";
  regexp = true;
  score = 7;
}

As mentioned before, this takes care of a very large portion of spam that wasn’t detected otherwise but is my no means the only thing you can tune. 

Tuning Symbol Scores

While looking at the history tab of rspamd’s web interface, I noticed certain symbols being added to emails which didn’t have enough weight to get the score over the threshold which I thought should be weighted higher. You can also manually paste the mail source into the form field in the “Scan/Learn” tab of the web interface to scan spam mails that have slipped through the filter to see what score the mail gets and what symbols where added. If you spot certain symbols over and over again and feel like they should be weighted more in the overall score, then head over to the Symbols tab and add custom scores to them.

There are so many symbols that I don’t remember which ones I have changed because I have used the web interface. I should’ve done that in a config file right away but too late now. You can be smarter than me and add a file local.d/scores.conf and add symbols and your custom scores as follows:

ONCE_RECEIVED = 5.0; 
MANY_INVISIBLE_PARTS = 5.0;

etc etc. 

Check/Configure the Fuzzy and Neural Modules

These modules are a cornerstone of rspamd’s effectiveness and therefore it’s worthwhile to check if they are indeed enabled and working. To do this run 

rspamadm configdump neural
rspamadm configdump fuzzy_check

For recommended values check out the module documentation of both. 

Ask the Mail Cow

Another great tip for getting more inspiration on how to fight spam with rspamd is to look into the repository of mailcow, which is a dockerized and pre-configured mail server setup and many of their configuration choices are proven to be solid. 

For example you can take a look at the entire local.d folder and get inspiration, e.g. for tuning the fuzzy module. Also for your postfix and dovecot configs you could get useful settings that might have not occurred to you. What I did was to look at their configs and when I saw options that sounded interesting and which I didn’t know, I looked them up in the postfix/dovecot/rspamd documentation to see if they’d be suitable for me as well.

I wouldn’t blindly copy all their settings because many might not apply to your scenario and without understanding what they do, you can make your setup worse or break it entirely. Don’t change too many things at once. Do one change at a time, test and confirm that they are working as intended. Use rspamd’s web interface to scan and check mails and to feed the fuzzy and neural modules.

Auto Learn From Users Spam

This is another great option for training your spam filter. There are ways to auto scan junk boxes and auto feed them to the rspamd but I am not using this as all the previous methods already work well enough for me. Spam mails are usually quite distinguishable from “proper” mail with all the previous methods mentioned – but if you have a medium to large multiuser setup with a diverse user base (region, language, age) you might be receiving very diverse spam and auto learning from user classified spam might bring the last few percent. 

You could even implement it in a way like gmail, by flaggin mail in user mail boxes after delivery, when enough users have marked it the same mail as spam. However there is a lot more effort required when you want to preserve data privacy which means a bit of scripting – but it is possible.

I hope that helps some of you to drastically reduce your spam. It did for me and I was surprised that some of the dullest methods were the most effective ones.

Questions?

I’m sure I haven’t answered all your questions and it’s not easy to cover everything. The rspamd config documentation isn’t easy to consume and to understand in its entirety and I wouldn’t claim I’ve reached the pinnacle of understanding but what I’ve done is enough so that I don’t get a single spam email into my inbox for days in a row. Whenever one slips through the cracks, I adjust one of the modules mentioned above.

Feel free to ask if you have any remaining questions in the comments or via the usual channels and let me know what things you have tuned to great effect. Sharing is caring 🙂

Oh and of course feel free to correct any errors I might have made!

Special thanks to @leah@chaos.social who saved my sanity during my config debugging session where I tried to figure out which modules are actually active and working.

Replacing the TouchMix DAW Utility

I’ve bought the QSC TouchMix 30 digital mixer a few years ago and I really like the device for many reasons. QSC’s software support isn’t one of them though.

The mixer allows you to record directly to a USB connected SSD (or fast USB Stick). It does so by putting the raw .wav files in a generic folder structure and saving a project xml file (project_name.tmRecord) that holds the track name / track number info as well as information about sections and markers.

To get the .wav files named after the track names in the mixer I’ve used QSC’s own tool called “TouchMix DAW Utility” which allows you to select source, destination, tracks to import and does the renaming of the .wav files according to the information in the .tmRecord file.

The tools was not update in years, it does not support dark mode, it is not apple silicone native and it copies the files rather slowly and sometimes even appears to be stalling.

Since I only record continuous sessions (full rehearsal room sessions) – I thought that it should be fairly simple to replace the sluggish and unmaintained tool with a simple shell script.

You can find the script on Github: https://github.com/hukl/qsc_touchmix_extract/

To use it, invoke it like this:

./qsc_tm.sh /path/to/project_name.tmRecord /path/to/destination/folder

If someone comes up with a more advanced version that properly deals with sub-regions and markers feel free to shoot me a PR.