Reversing the Scout ADS-B receiver (part 3)

When I last worked on this effort in October 2025, I had left off puzzled and frustrated that I had a decryption key and encrypted message traffic, but only one tantalizing successful decryption on hand. I eventually put the project aside for twelve months.

Returning with a fresh mind

My interest in the project returned when I realized I might be able to dump the firmware from the Scout itself. Using DuPont wires and a Raspberry Pi, I connected to the ESP8266 and extracted its four-megabyte flash image. This was satisfying but, for the immediate problem, something of a side quest. A raw embedded image (with few symbols, unfamiliar memory mappings, and none of the structure of a ‘normal’ application executable) proved much less approachable than the ForeFlight binary.

I became resigned to the fact that the ForeFlight app binary was a far more accessible and readable data source. But the hands-on act of firmware extraction rekindled my interest in the overall project and I returned to it with new enthusiasm.

I may write up the firmware-extraction process in a later blog post.

Decryption puzzle solved

The trick to understanding and solving the decryption problem was rooted in these facts:

  • AES operates on 16-byte blocks
  • The encrypted message payloads were 28 or 29 bytes long, not multiples of 16
  • The decryption routine at sub_100D8C194 operates on the trailing 16 byte window first

The key information was rooted in that last item. The tail 16 bytes are decrypted first, and that same 16 bytes are mutated in-place from ciphertext to plaintext. Only then is the first 16 bytes decrypted. And the critical piece to understand is that the final few bytes of that first portion’s ciphertext were embedded in the tail’s 16 bytes. This broad technique has a name: ciphertext stealing.

What “ciphertext stealing” means here

The sender had 29 bytes of plaintext but AES only handles 16-byte blocks. To avoid padding:

  1. Encrypt block 1 (the first 16 plaintext bytes), creates C1 (16 bytes).
  2. Save the first 13 bytes of C1.
  3. Paste the last 3 bytes of C1 into the start of the plaintext that will become block 2.
  4. Encrypt block 2, now transmit all 16 bytes.

The receiver reverses this:

  1. Decrypt block 2 (the last 16 wire bytes), which reveals the stolen 3 bytes + trailing plaintext.
  2. Reassemble C1 from the leading 13 wire bytes + the 3 recovered bytes.
  3. Decrypt C1, yields first 16 plaintext bytes.

Total ciphertext on the wire equals total plaintext; no padding is added or removed. The two decryption steps are dependent (step 2 feeds step 3), not independent.

Decrypting Scout/Sentry GDL 90 encrypted traffic: a complete worked example

Step 0: The raw message as captured off the wire

7e 26 00 d7 64 6f 8c 55 68 a1 cf 1d 4d 4d 85 21 92 0b 4c e7 00 ff bd 95 df 78 27 78 5c 94 15 5f be eb 7e

35 bytes on the wire, including the 0x7e framing flags, the 0x26 0x00 message marker, and the 0xbe 0xeb CRC.

Step 1: Strip the framing flags

The 7e bytes at both ends are delimiters; they tell the parser “a message starts/ends here.” Remove them.

26 00 d7 64 6f 8c 55 68 a1 cf 1d 4d 4d 85 21 92 0b 4c e7 00 ff bd 95 df 78 27 78 5c 94 15 5f be eb

33 bytes remain.

This particular packet contains no escaped bytes. In the general case, the receiver must now remove each 0x7d escape byte and XOR the following byte with 0x20. The CRC is checked only after this unescaping step.

Step 2: Check the CRC (do this before decrypting)

The last 2 bytes are a CRC-16 computed over everything before them. Peel them off and set them aside.

body: 26 00 d7 64 6f 8c 55 68 a1 cf 1d 4d 4d 85 21 92 0b 4c e7 00 ff bd 95 df 78 27 78 5c 94 15 5f
crc: be eb


CRC computation

Init 0, polynomial 0x1021 from table at 0x1050D5960, but with unusual recurrence. GDL 90 calls this a CRC-CCITT, although its published recurrence differs from the CRC-16/XMODEM implementation found in many libraries. That distinction matters: using a generic XMODEM routine produces the wrong checksum. The lookup table is the standard CRC-16/CCITT table (256 entries, poly 0x1021), but the incoming byte is XORed after the table lookup rather than being folded into the lookup index:

for each byte b:
crc = table[(crc >> 8) & 0xFF] ^ (crc << 8) ^ b

Compare with standard CRC-16/XMODEM, which folds the byte into the index before lookup:

    crc = table[(crc >> 8) ^ b]  ^  (crc << 8)

Same table, same polynomial, same init. The only difference is whether the byte enters before or after the table lookup. This produces a different checksum.

Step 3: Identify the header

26 00 d7 64 6f 8c 55 68 a1 cf 1d 4d 4d 85 21 92 0b 4c e7 00 ff bd 95 df 78 27 78 5c 94 15 5f
│ │ │
│ │ └── ciphertext starts here (29 bytes)
│ └────── unknown envelope byte; observed as 0x00 or 0x01
└────────── msgID 0x26 (proprietary; routes to the AES decrypt path)

0x26 is not a standard GDL 90 message ID, it is ForeFlight’s proprietary extension. In the binary it dispatches to handleGDL90Packet:atTime:, which invokes decryptPacket:withKey:. Labeled “encrypted envelope” here descriptively.

The second byte is never read by any code path in ForeFlight. In the original five samples it was always 0x00; in wider captures it also appears as 0x01. An alternating sequence flag is one possibility, but its purpose remains unknown.

Step 4: Split the ciphertext

AES operates on 16-byte blocks. The 29 byte ciphertext is clearly not a multiple of 16.

ciphertext (29 bytes):
[ d7 64 6f 8c 55 68 a1 cf 1d 4d 4d 85 21 ][ 92 0b 4c e7 00 ff bd 95 df 78 27 78 5c 94 15 5f ]
└──────── 13 bytes: leading fragment ──────┘└───────────── 16 bytes: one full block ────────────┘
(cannot be decrypted alone) (complete, can be decrypted)

Step 5: Decrypt the full block (the tail)

D( 92 0b 4c e7 00 ff bd 95 df 78 27 78 5c 94 15 5f )
== 1a 1c 76 ee 80 03 41 53 41 31 38 38 20 20 06 0a
──────── ─────────────────────────────────────
3 bytes 13 bytes
recovered trailing plaintext

The first 3 bytes of this output (1a 1c 76) are not plaintext! They are the last 3 bytes of the first ciphertext block that the sender placed here. The remaining 13 bytes (ee 80 03 41 53 41 31 38 38 20 20 06 0a) are genuine plaintext.

Why: the sender, before encrypting, took its full first ciphertext block C1 (16 bytes), split it, and pasted the tail 3 bytes into the second block’s plaintext. So decrypting the second block reveals those 3 bytes.

Step 6: Rebuild the first ciphertext block

C1 was 16 bytes. You have its first 13 (the leading fragment). The last 3 just came back out of step 5. Concatenate those two fragments:

C1 = [ d7 64 6f 8c 55 68 a1 cf 1d 4d 4d 85 21 ][ 1a 1c 76 ]
└──────────── leading 13 bytes ──────────┘└─recovered─┘
== 16 bytes. Now a complete AES block.

Step 7: Decrypt the rebuilt first block

D( d7 64 6f 8c 55 68 a1 cf 1d 4d 4d 85 21 1a 1c 76 )
== 14 00 ad 03 6b 21 e2 31 a9 05 09 0d 29 8a 0c bf

This yields 16 bytes of plaintext.

Step 8: Assemble the full plaintext

[ 14 00 ad 03 6b 21 e2 31 a9 05 09 0d 29 8a 0c bf ][ ee 80 03 41 53 41 31 38 38 20 20 06 0a ]
└──────────────── first block (16 bytes) ───────────┘└─────────── trailing 13 bytes ────────────┘
== 29 bytes of plaintext

Step 9: Decode the plaintext as a GDL 90 Traffic Report

GDL 90 message 20 (Traffic Report) layout per §3.5.1. The 29-byte plaintext:

14 00 ad 03 6b 21 e2 31 a9 05 09 0d 29 8a 0c bf ee 80 03 41 53 41 31 38 38 20 20 06 0a

Field boundaries (byte positions 1-indexed per spec):

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
14 00 ad 03 6b 21 e2 31 a9 05 09 0d 29 8a 0c bf ee 80 03 41 53 41 31 38 38 20 20 06 0a
|| || |------| |------| |------| |---| || |------| || || |---------------------| || ||
Byte(s)FieldHexDecoded
1Message ID1420: Traffic Report
2Alert / addr type00alert=0, addrType=0 (ICAO)
3–5ICAO addressad 03 6b0xAD036B
6–8Latitude21 e2 31+47.64876°
9–11Longitudea9 05 09−122.31609°
12–13Altitude + misc0d 294250 ft, misc=0x9
14NIC / NACp8aNIC=8, NACp=10
15–16Groundspeed0c bf203 kt
16–17Vertical ratebf ee−1152 fpm
18Track80180° (south)
19Emitter category03Large (75,000 to 300,000 lb)
20–27Callsign41 53 41 31 38 38 20 20ASA188··
28Priority / spare06no emergency; Scout extension marks byte 29 valid
29Geometric-altitude data0a+225 ft → geometric altitude 4,475 ft

This corresponds with a large, fast, descending aircraft heading south through the Boeing Field / Duwamish corridor, consistent with an arrival into Seattle.

Byte 29 is a proprietary Scout extension rather than part of the published GDL 90 traffic-report format. Reverse engineering ForeFlight’s parser showed that bit 1 of byte 28 marks the extension as valid. ForeFlight interprets byte 29 as a signed pressure-to-geometric-altitude adjustment, encoded in 25-foot increments with zero reserved.

Working proof-of-concept Scout/Sentry monitor

I have a Python program that receives the UDP port 4000 Scout broadcasts, decrypts them, and writes them to stdout as well as optionally to a file for later work. In practice on the ground I’ve successfully received and decoded:

  • Ownship reports
  • Scout heartbeats
  • Device messages
  • GDL 90 traffic reports (0x14 / decimal 20)
12:19:54.408 TRAFFIC callsign=N456N addr=A58992 lat=47.700255 lon=-122.356260 alt_pressure_ft=1600 alt_geo_ft=1975 speed_kt=80 track_deg=329.06 vertical_fpm=256 airborne=yes nic=9 nacp=10
12:19:54.413 TRAFFIC callsign=ASA594 addr=AD237C lat=47.652490 lon=-122.316027 alt_pressure_ft=3600 alt_geo_ft=4025 speed_kt=174 track_deg=180 vertical_fpm=-640 airborne=yes nic=8 nacp=10
12:19:55.427 HEARTBEAT initialized=yes gps_valid=no utc_valid=no anonymous=yes device_time=unavailable uplink_count=0 traffic_count=0 extra=000000000000000000
12:19:55.427 DEVICE model=Scout hardware_id=11 firmware=2.3.6 serial=329163165 secondary=absent
12:19:56.452 OWN callsign=SELF addr=000000 position=unavailable alt_pressure_ft=0 speed_kt=0 track_deg=0 vertical_fpm=0 airborne=no nic=0 nacp=0

The traffic reports directly correlate with aircraft overhead (observed in FlightRadar24.com).

The Scout specifications indicate that it should also emit standard GDL 90 Uplink Data messages (0x07, decimal 7), whose 432-byte UAT payload can carry FIS-B weather products. I have not yet received one at ground level from my location, so that remains untested, and gives me an excellent excuse for a future aerial capture.

For now, though, the original puzzle is solved: the supposedly proprietary traffic packets are ordinary GDL 90 reports wrapped in a small encrypted envelope, and a discontinued receiver that had been sitting in my drawer is once again producing useful data.

Reversing the Scout ADS-B receiver (part 2)

With apologies, here is a very late writeup about reverse-engineering efforts I undertook in October, 2025.

When I left off last time in Part 1, I had captured the actual wire data flowing between the Scout device and ForeFlight. Analysis made it clear that they were using a non-standard extension to the GDL90 spec.

If ForeFlight could decode these packets, its own code had to know their format. I dove into the ForeFlight application binary using IDA.

Hunting for Clues

My entry point was a search for specific literal numeric constants within the binary. I began by searching for 0x7e, the byte that marks the beginning and end of a GDL 90 frame. There were plenty of matches, so I sorted them by function name. ForeFlight’s own classes often begin with FF, which gave me a useful trail to follow.

One result led to a class named [FFGDL90RawStreamParser parseGDL90Data:fromHost:atTime:], which I took to be a good sign. Searching for 0x26 was even better. IDA landed in [FFScoutExternalDevice handleGDL90Packet:atTime:] and showed a large switch statement dispatching on the message ID. This was the “bingo!” moment; I had found the central dispatcher for these packets.

Decoding the Identification Message

As I began renaming variables to make the logic more legible, I noticed that message ID 0x26 (decimal 38) appeared to be encrypted and triggered a “non-open GDL 90 mode,” implying a proprietary or non-standard implementation.

However, other messages were clearer. For instance, case '%' corresponded to message ID 0x25, which handles Scout identification. By tracing the calls to parseIdentificationMessage and parseDeviceIDFromData, I was able to correlate the binary data with known device information. The first two bytes were the message ID 0x25 and a still-unexplained 0x01. The twelve-byte primary device record began immediately afterward::

Record offsetLengthMeaningCaptured value
03Firmware version02 03 06 → 2.3.6
31Hardware ID0b → 11
48Serial number, little-endian9d a1 9e 13 00 00 00 00 → 329163165
1212Secondary-device recordAll ff, apparently absent

The Search for the Decryption Key

The real challenge was decrypting the 0x26 packets. I began exploring FFCommonGDL90PacketHandler handleGDL90Packet:atTime: and discovered that FFScoutExternalDevice reuses the SentryPacketDecryptKeyProvider.

Digging into the initialization of the defaultSentryPacketDecryptKeyProvider, I found a reference to a specific memory location: xmmword_105268E00.

__const:0000000105268E00 xmmword_105268E00 DCB 0xA6, 0xB1, 0xC0, 0x1F, 0x22, 0, 0x56, 0x62, 0x68
__const:0000000105268E00 ; DATA XREF: -[defaultSentryPacketDecryptKeyProvider init]+44↑o
__const:0000000105268E00 ; -[defaultSentryPacketDecryptKeyProvider init]+4C↑r
__const:0000000105268E09 DCB 0x93, 0x7C, 0x70, 0x8A, 6, 0xDD, 0xCF

Note these 16 bytes of data:

A6 B1 C0 1F 22 00 56 62 68 93 7C 70 8A 06 DD CF

The array was exactly 16 bytes long — the size of an AES-128 key — and the surrounding code used it through something named SentryPacketDecryptKeyProvider. That made it a very strong candidate.

Headway on Understanding the Cipher

The breakthrough came when analyzing sub_100D8C194 (called via a chain of functions starting from [FFUAvionixGDL90PacketParser decryptPacket:withKey:]). The call gave away the cipher: AES with a 16-byte key, ECB mode, and no padding; in other words, raw AES-128-ECB. (The null IV argument was a red herring; ECB does not use an IV.)

CCCrypt(
1u, // Operation: Decrypt
0, // Algorithm: AES
2u, // ECB mode
*p_aes_key, // Key
0x10u, // Key length: 16
0, // IV: 0
&v8[in_buflen - 18], // Final 16 bytes before CRC
0x10u, // Data in length: 16
&v8[in_buflen - 18], // Data out
0x10u, // Data out length: 16
dataOutMoved), // Result length

Testing this theory, I entered the suspected key and a captured ciphertext sample (446e434cc35729cf1954ac1a6db9f2f9) into CyberChef. The result was a clean decryption to 00110000000000000000000000000000. The first decrypted byte was 0x00, the standard GDL 90 heartbeat message ID. The next byte, 0x11, contained its status flags; the remainder of this particular block was zero.

Efforts on Expanding the Decryption

Despite the initial success, no other message sample decrypted cleanly. I experimented with reversing the key and transposing its halves, but no variant was successful. Besides, no particular gymnastics were required with my successful decryption above, so I became quite puzzled here.

The decompiled CCCrypt() call contained the clue I had missed. The expression &v8[in_buflen - 18] selected the final sixteen bytes before the two-byte CRC. At the time I did not understand why that mattered. As we’ll see in the next part, it was the critical detail!

Reversing the Scout ADS-B receiver (part 1)

TL;DR: the Scout ADS-B receiver transmits its data using a proprietary, undocumented format that I cannot decode, but perhaps with scrutiny it could be decoded and used for fun projects. Come for the story of my process; stay for the crappy responses from ForeFlight and uAvionix tech support.  This article is part 1, wouldn’t it be nice if there were a part 2…?

The uAvionix Scout is a dual-band ADS-B receiver designed for use in private aviation. For those not familiar, ADS-B is a technology used to communicate traffic and inflight weather information to pilots; a typical use-case is to enhance the pilot’s situational awareness by displaying this info on the pilot’s iPad which is running an “electronic flight bag” app like ForeFlight. (ADS-B data itself is free to use, is subscriptionless, and is transmitted from FAA stations, all of which is paid for with your tax dollar.)

Although the Scout device has been discontinued you can find them on eBay for less than $200 US. It’s a compact piece of gear, about the size of a pack of gum, and is powered by 5V micro-USB.

ForeFlight summarizes its feature set nicely on https://foreflight.com/support/scout/ :

Scout provides dual-band ADS-B In capabilities. With antennas for both the 978 MHz and 1090 MHz frequency bands, Scout enables the display of inflight weather and traffic on ForeFlight Mobile. Scout supports the full range of FIS-B weather and data, including animated regional and CONUS NEXRAD, METARs, TAFs, AIR/SIGMETs, PIREPs, winds and temps aloft, TFRs, NOTAMs, and SUA information. Scout can also receive air-to-air traffic information from ADS-B Out equipped aircraft and re-broadcast traffic information (ADS-R and TIS-B) from FAA ground towers. Keep in mind that if your aircraft is not equipped with ADS-B Out, then you will have a limited view of traffic on ForeFlight.

That’s a wealth of data that would be wonderful to read and display.

I acquired a Scout some time ago and used it happily prior to upgrading to a more featureful Stratux unit. Afterward the Scout languished in a drawer until I wondered if it would be possible to use the Scout data on the ground, outside the cockpit.1

The typical way a ForeFlight user utilizes the Scout (or the Stratux) is over Wi-Fi: the Scout maintains a low-power open network which the iPad joins. Upon powering the unit the Scout advertises an unencrypted Wi-Fi access point with ESSID “Scout-A702” in my case – and of course any device can join, not just iPads.

I attached to the Wi-Fi with my computer and fired off nmap scans of TCP and UDP to see what ports might be available. The Scout was at 192.168.4.1, and it provided my computer the IP address 192.168.4.100 via DHCP.

$ nmap -sT 192.168.4.1
Starting Nmap 7.92 ( https://nmap.org ) at ___
Nmap scan report for 192.168.4.1
Host is up (0.046s latency).
Not shown: 999 closed tcp ports (conn-refused)
PORT STATE SERVICE
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 0.80 seconds

$ sudo nmap -sU 192.168.4.1
Starting Nmap 7.92 ( https://nmap.org ) at ___
Nmap scan report for 192.168.4.1
Host is up (0.019s latency).
Not shown: 999 closed udp ports (port-unreach)
PORT STATE SERVICE
4000/udp open|filtered icq
MAC Address: a:b:c:d:e:f (Unknown)
Nmap done: 1 IP address (1 host up) scanned in 1.85 seconds

I would come back to UDP 4000 in a bit; I wanted to examine that webserver first.

It turns out that there is a very tiny web page there:

Interesting. Those two Update hyperlinks point to /update and /pingUpdate. Those might be worth exploring later. ForeFlight notes, “You can update Scout’s firmware directly through ForeFlight.” Perhaps ForeFlight uses these two HTTP endpoints for this purpose.

Before proceeding I ran feroxbuster (a Rust-based utility similar to gobuster or dirbuster) to try to discover any hidden API endpoints on the unit. I was unable to find any additional endpoints using this technique.

feroxbuster -d 1 --url http://192.168.4.1 -w SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-small.txt

The next logical step was to let ForeFlight and the Scout talk to each other and observe their network traffic. The observation computer was Linux-based, so it was a trivial matter to put its Wi-Fi adapter in monitor mode, set it to channel 1 (the channel of Scout-A702), and begin monitoring the 802.11 traffic between ForeFlight and the Scout.2

The first thing I noticed was ForeFlight advertising this JSON string as a UDP broadcast on port 63093 every five seconds:3

{"App":"ForeFlight","GDL90":{"port":4000}}

Interesting. ForeFlight was advertising some sort of service or API on UDP port 4000, the same open port that nmap revealed on the Scout. I also noticed ForeFlight regularly broadcasting a curious string:

i-want-to-play-ffm-udp

A little googling brought me to this page: https://www.foreflight.com/connect/spec/ , “GDL 90 Extended Specification”. Being unfamiliar with said spec I did more googling; it turns out that GDL 90 is a Garmin aviation ADS-B datalink transceiver as well as a data interchange specification apparently based on this device. 

The Specification website above said,

ForeFlight offers the industry-standard GDL 90 Data Interface Specification defined below for third-party devices to transmit live inflight data to ForeFlight Mobile. Properly configured devices will be able to display ADS-B weather and traffic, AHRS, device name information, and GPS data in the ForeFlight Mobile app.
…
ForeFlight expects data sent using UDP to port 4000 on the iOS device.

The website went on to describe the ForeFlight broadcast JSON on port 63093 that I noted with Wireshark, as well as the message specification. This was exciting! This might be the information I needed to decipher the messages flowing between ForeFlight and the Scout. On this site was a link to a FAA-hosted Garmin PDF titled “GDL 90 Data Interface Specification”. I saved this for later reference.

I continued to monitor the connection in Wireshark. The UDP traffic flowing from the Scout into ForeFlight port 4000 had evident UDP payloads but of differing lengths: 34 bytes, 35 bytes, and 52 bytes.

I referred to the Garmin PDF mentioned earlier and discovered information about decoding the payload in section 2.2.1:

The basic message structure is as follows (see Figure 1): 1) A Flag Byte character (0x7E). 2) A one-byte Message ID which specifies the type of message being transmitted. 3) The Message Data, which can be of variable lengths. 4) A message Frame Check Sequence (FCS). The FCS is a 16-bit CRC with the least significant byte first. 5) Another Flag Byte character (0x7E). 

Observing the captured payloads above it was clear that these adhered somewhat (but not totally) to the Garmin spec: they began and ended with 0x7e as specified, but the second byte (the identifying Message ID) was a puzzler: in each case it is 0x26 (decimal 38). The Garmin document stipulated a number of Message ID values in section 3, “Message Definitions.” None of these were 0x26 / decimal 38. 

I referred back to ForeFlight’s “Extended Specification” website discovered earlier. It too did not define any message with ID 0x26. (It did, by implication, indicate that ForeFlight was happy to extend the Garmin messaging spec with their own message types. Clearly I was observing undocumented extensions.)

I googled further and stumbled across an IEEE paper titled GDL90fuzz: Fuzzing – GDL-90 Data Interface Specification Within Aviation Software and Avionics Devices — A  Cybersecurity Pentesting Perspective available at https://ieeexplore.ieee.org/document/9709804 . This was an interesting analysis in its own right and I encourage you to read it; there’s some impressive work done therein.

The paper yielded this notable gem that was relevant to my Scout work:

Some vendors have their own interpretation of the protocol outside of the Garmin standard. For example, uAvionix’s SkyEcho2 mainly uses the standard messaging types, but it outputs its ownship message with the message type code 101. On the other hand, ForeFlight’s Sentry extends the protocol and does not communicate with the standard message types. Sentry transmits messages with IDs 37 and 38, which are longer than the standard heartbeat, ownship, and traffic messages and most likely contain multiple message types in a single packet.4

The IEEE researchers had seen exactly what I was seeing: message traffic using undocumented / nonstandard IDs.

I also belatedly realized that the 52-byte message above was in fact two concatenated messages in one payload: ID 38 followed by ID 37.

Further research turned over no new data on what these undocumented messages might contain. I decided to try a bit of a Hail Mary: I would ask the vendors if they had any documentation to share. (It seemed like a reasonable request; after all, ForeFlight maintains a nice public website with their “extended specification.”)

I emailed the support addresses of these two firms:

Hi. I’m trying to find documentation on the GDL90 protocol extensions embraced by the Scout and Sentry products — in particular I’m seeking documentation on the nonstandard message IDs 37 (0x25) and 38 (0x26). 

I’ve consulted Foreflight’s page here : https://www.foreflight.com/connect/spec/ but unfortunately it doesn’t mention these two message IDs.

Are you able to forward me some documentation on this, please?

The responses I got back seemed to be wary and dripping with suspicion. ForeFlight sulkily quizzed me on where I had found the nonstandard messages and chided me that their existing public documentation was complete. I replied with my Wireshark data; ForeFlight then shut down the conversation:

Thank you for your request. The messages you are inquiring about are part of the proprietary data sent between ForeFlight Mobile and the Sentry line of products that we co-developed with uAvionix. Unfortunately, we are unable to provide additional documentation regarding these UDP messages.

uAvionix was even less friendly, replying tersely:

Thank you for contacting uAvionix, unfortunately we are not able to provide information on those proprietary messages.

So there you have it. The Scout (and presumably Sentry) communicate with ForeFlight using a private messaging extension to the open GDL-90 spec. It’s a shame that neither firm will disclose the contents or formats of these messages, as it would be delightful to decode them and use them for other fun tasks. I would love insight into the byte layout of messages 0x25 and 0x26.

  1. Yes, I understand that receiving ADS-B data on the ground can be challenging; ADS-B works best at altitude when in proximity to an ADS-B ground station. It is still possible to receive broadcasts from in-range aircraft and rebroadcast ground station data. It really just depends on location factors. ↩︎
  2. Could I have just associated the computer to the Scout’s open network? Sure. But in my experience promiscuous-mode wifi capture doesn’t always work perfectly; monitor mode is a few extra steps but more reliable. ↩︎
  3. Unlike TCP, UDP can broadcast since it is connectionless. ↩︎
  4. The IEEE paper also mentions, “When the app is accepting traffic, it sends ‘i-want-to-play-ffm-udp’; and when it goes to sleep it sends ‘i-cannot-play-ffm-udp.’” I observed the former but not the latter. ↩︎

Edit 15-Oct-2026: I’ve added https://cbpowell.wordpress.com/2026/10/15/reversing-the-scout-ads-b-receiver-part-2/ and https://cbpowell.wordpress.com/2026/10/15/reversing-the-scout-ads-b-receiver-part-3/ where I describe how I learned the decryption key and subsequently used it to decipher the Scout’s networking traffic.

Monte Carlo Analysis of Chutes & Ladders with Python

If you spend time around young children, sooner or later you’ll end up playing the game Chutes and Ladders. This game actually has quite an interesting history, dating back to ancient India. The Milton-Bradley version dates from the 1940s.

The game’s history is probably the most interesting thing about the game, beause it is utterly devoid of strategy or technique. The players’ only interaction with the game is to spin a spinner that provides a random number from 1 through 6; there is no skill aspect, there are no decisions to be made. And certainly, while perfectly enjoyable as a family pastime that brings people together, in and of itself the game offers little intellectual distraction.

This intellectual idle-time led me to think about the nature of the board. I wondered if the game, being entirely stochastic, had any hidden patterns within. After all, it’s reasonable to assume that Milton-Bradley didn’t have access to any sort of automated analysis of their game back in 1943. I wondered things like:

  • What’s the average number of spins to win?
  • How many chutes and ladders will a user typically hit?
  • Are the chutes and ladders balanced?
  • Are some squares hit more often than others?

What’s neat is the nature of the game lends itself well to numerical analysis. There’s no human component that requires estimation or approximation. Literally the only action a player takes is to spin a spinner. Why not use Monte Carlo analysis to play bulk trials of the game and look for answers?

Coding this in Python was fairly straightforward. Although not the most performant language, Python’s excellent visualization tools make it the language of choice for this project. Using joblib to parallelize the task meant that I could simulate 500,000 single-player games in 7 seconds. (Why single-player? The game doesn’t change at all by the number of players. There are no exclusions or conflicts or indeed any alterations at all, from a numerical analysis perspective.)

The results are kind of interesting if you’re the sort of person who wonders about these sorts of things.

For half a million games, I saw this:

7.633320899999944 seconds elapsed time
Minimum number of spins to win: 7
Maximum number of spins to win: 446
Arithmetic mean number of ladders hit by player: 3
Arithmetic mean number of chutes hit by player: 4
Arithmetic mean of spins to win: 40.092362
Median of spins to win: 33.0
Standard deviation = 25.522641855046196

There’s more information to be found in the visualizations:

Noting the average number of spins to win the game is 40, the cumulative number of spins to win a game rises in a smooth curve. You can see, for example, that the probability of concluding a game between 50 and 60 spins is about 81-82%, and that most games are won under 40 spins (the median number-of-spins-to-win being 33).

The heatmaps of board positions are particularly interesting to me. There are two heatmaps of note:

  1. a heatmap of where the user lands initially after spinning (i.e. before following any chute/ladder)
  2. a heatmap of where the user ends up at the conclusion of their turn (i.e. after following any chute/ladder)

The former shows two notable darker/cooler bands across the board: along the bottom, and in the upper third. These cooler bands are areas of infrequent landing. Once chutes/ladders are taken into account, however, the picture becomes much noisier. The squares with ‘0’ values may seem counter-intuitive until you realize those are the heads of chutes or bottoms of ladders — no turn ever ‘ends’ on these squares. This heatmap shows some particular hotspots such as the top of the “big ladder” and the bottom of Square 48’s yellow chute. Also note how it’s very, very common to get stuck at square 99, waiting for an exact spin of ‘1’ to finally win the game.

If you care to try this out locally, the source code is on Github.

On the ground at CppCon 2014

I’ve just returned from the week-long CppCon 2014 in Bellevue, Washington. Here’s what I experienced.

I’ve absorbed a great deal from a variety of C++ developer conferences – CppNow, Going Native, C++ And Beyond – but always virtually, via video and webcast. This was an opportunity to jump into the thick of things and participate in person. With community heavyweights like Herb Sutter and Scott Meyers in attendance I knew the content would be stimulating and informative. (Honestly, the speaker list featured nearly every name in the “C++ royalty” that you could imagine. I smiled to myself seeing Bjarne Stroustrup standing in the registration line like he was just another attendee.) So when the conference’s early-bird admission opened in March, I eagerly sent in my hard-earned dollars and blocked off the week of September eighth on my calendar.

Continue reading →

Unit test your private methods for great justice

The continuing dissent and confusion about unit testing of private class methods surprises me.

The access specifier is much like your choice of software license: it exists to limit consumers’ actions, not to limit yours. A method’s access specifier is completely irrelevant to testing, and only describes what you want the consumer to use; any code that takes inputs and produces outputs, private or not, should be tested.

The opponents of private-method testing tend to argue in quasi-religious terms: that private methods are mere hidden implementation details; that users of the class will only care about the public API; that testing of private methods breaks encapsulation. A typical unhelpful “solution”: private methods should be put into a different class and made public there.

To argue against granular testing of private methods is to mean well while being thoroughly unhelpful. The purpose of testing is more than just to guarantee the viability of your public interface – it is also to examine the inner machinery and support routines of your class to ensure that they themselves function correctly for a spectrum of inputs and edge cases. The private implementation will contain non-trivial complexities that are more readily and precisely tested directly than via the public API.
Continue reading →

Beautiful logging for Ruby on Rails 4

In a previous post I showed you a simple way to get beautiful, easy-to-read logs in your Rails 3.2 application. Rails 4 changed the game again; for Rails 3.2 or earlier, refer to my earlier post; but for Rails 4 read on…

It’s really easy. Just make a new file in your ‘config/initializers’ directory called something like ‘log_formatting.rb’ and paste into it the following code. Restart your app, and voila: pretty logs again!

UPDATED. Konrad’s comment below was correct. I’ve altered this code to work with both the regular logger and the new tagged logger. Now you can configure your logger as

config.logger = ActiveSupport::Logger.new('your_app.log')

or

config.logger = ActiveSupport::TaggedLogging.new(Logger.new('your_app.log'))

… both will work. Here’s the updated monkey patch:

class ActiveSupport::Logger::SimpleFormatter
  SEVERITY_TO_TAG_MAP     = {'DEBUG'=>'meh', 'INFO'=>'fyi', 'WARN'=>'hmm', 'ERROR'=>'wtf', 'FATAL'=>'omg', 'UNKNOWN'=>'???'}
  SEVERITY_TO_COLOR_MAP   = {'DEBUG'=>'0;37', 'INFO'=>'32', 'WARN'=>'33', 'ERROR'=>'31', 'FATAL'=>'31', 'UNKNOWN'=>'37'}
  USE_HUMOROUS_SEVERITIES = true

  def call(severity, time, progname, msg)
    if USE_HUMOROUS_SEVERITIES
      formatted_severity = sprintf("%-3s",SEVERITY_TO_TAG_MAP[severity])
    else
      formatted_severity = sprintf("%-5s",severity)
    end

    formatted_time = time.strftime("%Y-%m-%d %H:%M:%S.") << time.usec.to_s[0..2].rjust(3)
    color = SEVERITY_TO_COLOR_MAP[severity]

    "\033[0;37m#{formatted_time}\033[0m [\033[#{color}m#{formatted_severity}\033[0m] #{msg.strip} (pid:#{$$})\n"
  end
end

Steering Behaviors for Ruby game dev now on Github, Rubygems

I just released my Steering Behaviors package to Github, and an accompanying Gem to Rubygems.

If you’re building a game, you’ll want your game agents and characters to exhibit realistic motion. A standard way of doing this is with ‘steering behaviors’.

The seminal paper by Craig Reynolds established a core set of steering behaviors that could be utilized for a variety of common movement tasks. These include such behaviors as predictive pursuit, fleeing, arrival, and wandering. This Ruby library can accomplish many/most of those tasks for your Ruby / JRuby game.

The basic behaviors can be layered for more complicated and advanced behaviors, such as flocking and crowd movement.

Embellishments and expansions are planned, but this is working software you can use to drive your own game’s characters. (I’m using it in my own game programming.) The Github repo includes working graphical examples, and you can install the Gem for easier and more direct use in your own game.

Pull requests are enthusiastically encouraged.

 

On Github: my Fuzzy Associative Memory (FAM) package

I’ve just released to Github my working fuzzy logic module, Fuzzy Associative Memory.

A Fuzzy Associative Memory (FAM for short) is a Fuzzy Logic tool for decision making. It uses Fuzzy Sets to establish a set of rules that are linguistic in nature; examples might include:

  • “If the room is a bit warm, turn the fan up a little bit”
  • “If the orc’s hit points are a little low, retreat from the enemy”
  • “If the ship is off course by a little bit, correct just a little to the right”
  • “If the bird is much slower than the flock, speed it up a lot”

As you can see, the rules are deliberately vague and use qualifiers like “a little” and “a lot”. This is the nature of fuzzy sets; they capture such human fuzziness in a way that extracts highly natural behavior from the fuzzy rules.

It has a wide range of applications:

  • Industrial control, such as governing a fan to keep a room at the “just right” temperature
  • Game AI, such as giving human-like behavior capabilities to NPCs
  • Prediction systems

Status

This is working, functional software. It currently supports:

  • Triangular fuzzy sets for input/output
  • Larsen Implication (scaling)
  • Atomic antecedent propositions (if A then Z)

To do:

  • Trapezoidal (and other shapes) for fuzzy sets
  • Hedges (‘very’ and ‘fairly’)
  • Mamdani Implication (clipping)
  • Composite antecedent propositions (if A or B, then Z)
  • Additional examples

Examples

The bin directory contains the following examples:

  • hvac_system_example illustrates how a FAM could govern an HVAC fan unit to maintain a constant, comfortable temperature

Get it here!

Entity-Component game programming using JRuby and libGDX – part 8

Introduction

Our Lunar Lander game is somewhat playable by this point but it still lacks some key features. After all, it would be nice if we could detect collisions and determine if the lander has safely landed on the pad. Let’s see how our flexible Entity-Component system permits us to expand our game with minimal fuss.

Collision Detection

First, a frank disclaimer: the following collision detection algorithm is entirely inefficient. It’s kept simple for our basic teaching purposes here but is probably undesirable in a game of any scale. But that’s OK: E-C will permit you to swap in a much more advanced collision detection system when you’re ready. 🙂

Read the rest of this story…