SAM DUMONT

Freelance Cloud & AI Platform Engineer | ex-AWS
Data platforms · Garmin apps · Sensor firmware · Remote-first

Laeken, Belgium

// BLOG

Reverse-engineering Race-Keeper's .rkd binary format in one afternoon

Five years of telemetry from supercars, locked in a proprietary format I couldn’t read. macOS user, Windows-only player, no documentation. I’d tried to crack it in 2021 and given up.

The setup

In April 2021, I got a track day at Circuit de Mettet in Belgium as a birthday gift. An Audi R8 V10 and a Lamborghini Huracán, complete surprise. The track uses Race-Keeper “Instant Video” systems: synchronized multi-camera video with GPS, accelerometer, and gyroscope data. At the end of the day, you get a USB stick with a Windows player that overlays gauges on the footage: speed, track map, g-forces. Professional data from a birthday gift I didn’t know I was getting.

One problem: the readme on the USB helpfully says “This software is not Apple Mac compatible.” The video files play fine anywhere, but the telemetry lives in proprietary .rkd binary files. No documentation, no third-party tools, barely any mentions of the format online. I tried to decode it manually a few years later, couldn’t make sense of the hex dumps on my own, and moved on. That data sat on my drive for almost 5 years.

The USB stick

The USB stick itself tells you a lot before you open a single binary:

USB Root/
├── autorun.inf
├── PPKVIDEO/
│   ├── PPKVIDEO.exe          # Qt4-based video player
│   ├── RK-ExportTool.exe     # Data export utility
│   └── *.dll                 # Qt4, ffmpeg, libvideo
├── 06107796_R8V10_11098_20210404_130024/
│   ├── video.mp4
│   ├── outing.rkd            # ← the telemetry file (474 KB)
│   ├── video_hrd.bin
│   └── trivinci_*.log
├── 06107796_HUR_11133_20210404_131601/
│   └── ...
└── Play Video.bat

The folder naming convention is {SERIAL}_{CARNAME}_{CARID}_{DATE}_{TIME}. Same device serial for both sessions, car names matching the vehicles (R8V10, HUR), and the Trivinci diagnostic logs pointing to the manufacturer: Trivinci Systems LLC. The .rkd extension: Race-Keeper Data. All of this becomes useful later when you’re staring at hex values and need anchors to validate against.

The approach

I pointed Claude Code at the hex dump and said “what do you see?” It identified the file header and proposed a record structure. From there it was a loop: Claude proposes something, I validate or redirect with domain knowledge, Claude adjusts and tests in seconds.

The split felt natural. I had the physical reality: what speeds and g-forces make sense for these cars, where the circuit is, what the GPS coordinates should look like. Claude had the speed: testing dozens of encoding hypotheses in seconds, writing conversion scripts on the fly, computing haversine distances between GPS coordinates. What would have taken me 20 minutes of scripting per hypothesis just happened instantly.

About 4 hours across three sessions to go from “unknown binary” to “fully validated parser with exports.”

The format

The RKD format turned out to be well-designed. The developers clearly knew what they were doing.

Magic bytes

The very first thing you do with an unknown binary is look at the header:

00000000: 89 52 4B 44 0D 0A 1A 0A  .RKD....

That’s a PNG-style magic signature: \x89RKD\r\n\x1a\n. The high bit catches 8-bit stripping, \r\n detects newline conversion, \x1a stops the DOS TYPE command. Classic embedded systems engineering: whoever designed this format had read the PNG spec.

Record structure

After the 8-byte magic comes a 28-byte meta header. Reading it as seven uint32 values gave us 1343488, 0, 1, 0, 11098, 1617523240, 0. That 11098 jumped out immediately: it matched the _11098_ in the folder name on the USB stick. First anchor point. And 1617523240 as a Unix timestamp converts to April 4, 2021: the session date. Two confirmed fields from one line of hex dump. Everything else clicked faster after that.

The rest of the file is a stream of typed records. Each record has a 10-byte header: five uint16 fields for CRC, type, payload size, and a 32-bit frame number split across two 16-bit words.

FieldSizeDescription
CRC2 bytesRecord checksum
Type2 bytesRecord type identifier
Payload size2 bytesBytes following this header
Frame (low)2 bytesVideo frame number, low 16 bits
Frame (high)2 bytesVideo frame number, high 16 bits

Seven record types in total: config strings, GPS, accelerometer, gyroscope, periodic system metrics, hardware timestamps, and a session terminator. The first records after the header are Type 1: null-terminated key-value config strings.

CAPTURE_VERSION\03.13.0\0
BUILD_DATE\0Jan 30 2021 18:11:52\0
ENCODER_DEVICE\0TI8168 Rev 2.0\0
VIDEO_BITRATE\08000000\0
ACCEL_Z\0171.875\0
OUTING_GUID\0{EA125A64-...}\0

About 40 of these per file. The TI8168 is a TI DaVinci video encoder (so we know the exact SoC), the accelerometer calibration offsets are stored in milli-g (171.875 mg), and each session gets a unique GUID. Free anchors for validation, and they tell you a lot about the hardware.

GPS at 5 Hz

Type 2 records: 36-byte payloads, appearing every 6th frame (30 fps video ÷ 5 Hz GPS = every 6 frames). The first field was always 03 00 00 00: a 3D fix indicator. Standard stuff.

The timestamp was the first real puzzle. The value 1301565641 was too large for Unix (which would be ~1.6 billion for 2021) but too small for microseconds. I know GPS chipsets use their own epoch: seconds since January 6, 1980. Claude tried it, subtracted the 18 leap seconds, and the result was 12:00:23 CEST on April 4, 2021. Session time confirmed.

Coordinates: the value 503010636 divided by 10,000,000 gives 50.3010636°. That’s Circuit de Mettet. Same 1e7 divisor for longitude. This is the u-blox UBX protocol format: standard GPS encoding, confirming the hardware uses a u-blox or compatible receiver.

Speed took some detective work. The raw value was 2409:

  • 2409 km/h? No.
  • Divide by 10: 240.9 km/h? Too high for mid-corner.
  • Divide by 100: 24.09 m/s = 86.7 km/h. Realistic.

Centimeters per second. Validated by computing GPS-derived speed from consecutive lat/lon positions using haversine: the values matched within 1% across thousands of data points. Max speed in the R8 file: 156.5 km/h, matching what I remembered from a careful first session (5 km total, the instructor patiently calling “hop, voilà, parfait” through every corner). The Huracán was a different animal: the instructor says it himself at 2:49 in the video, “c’est autre chose que la R8.” The telemetry confirms it: 67 to 189 km/h in 7 seconds through three gears, 9 km total distance, 198.4 km/h top speed.

Heading and altitude followed the same pattern. The raw heading value 3,188,000 divided by 100,000 gives 31.88° (northeast, correct for that section of track). Altitude raw value 256,600 divided by 1,000 gives 256.6 meters: Circuit de Mettet sits at 248-265m on topographic maps.

IMU at 30 Hz

Types 7 (accelerometer) and 12 (gyroscope): 12 bytes each, three int32 values, one per video frame.

The accelerometer Z-axis was the giveaway. Values clustered around ~1000 at rest. That’s 1g in milli-g encoding (1000 mg = 9.81 m/s²). The mean Z-acceleration across the entire R8 session: exactly 9.81 m/s².

The gyroscope was harder. Same 3×int32 structure but different value ranges. We compared the yaw rate (Z-axis) against GPS heading change rates and tested divisors from 1 to 100. At a factor of ~28 raw units per degree/second, the gyro values matched the GPS-derived turn rates. This is an empirical calibration: it may vary with hardware revisions, but it’s consistent across both sessions in my data.

Cross-validation

I cross-checked everything against physical reality:

  • GPS speed vs. haversine speed: consecutive positions → distance ÷ time. Error under 1% across thousands of fixes.
  • Z-axis accelerometer mean: 9.81 m/s² across a full session. EXACTLY 1g.
  • Altitude range: 249-265m with a repeating pattern per lap. Matches the circuit’s topography.
  • GPS coordinates: plotting all fixes produces a recognizable track layout that matches satellite imagery.
  • Max speed locations: 156.5 km/h (R8) and 198.4 km/h (Huracán) both occur at the same GPS coordinates on the main straight, but they represent completely different intensity levels: 5 km of learning vs. 9 km of committed driving.

The hardest braking event in the Huracán session: 1.3g longitudinal at 6:35, during a 189 → 64 km/h braking zone. Around 6:00 the instructor goes quiet for a full lap (“je dis rien”): that’s where 198 km/h happens. His only comment after the big braking event: “c’est du sport auto.”

The record counts confirm internal consistency too. The R8 session runs 289 seconds: at 5 Hz that predicts 1,445 GPS fixes (actual: 1,457), at 30 Hz that predicts 8,670 IMU records (actual: 8,681). The small overshoot is expected from partial intervals at session boundaries. The format is clean.

The result

Python + Go parser, both producing byte-for-byte identical CSV and GPX output. 77 tests on Python (100% branch coverage), 76 on Go. Zero external dependencies in either language. The first public documentation of the RKD binary format: full spec and research notes.

The CSV export works directly with Telemetry Overlay at 30 Hz with GPS interpolation, so any Race-Keeper recording can get custom data overlays on any platform. Five years later, my birthday gift finally has proper video overlays.

Both sessions are up on YouTube with the overlay data: the R8 V10 and the Huracán. The subtitle timestamps map to the 30 Hz telemetry CSV, so you can cross-reference every instructor callout with actual numbers.

The project is open source on GitHub. If you have a Race-Keeper system, it should just work. And if you have .rkd files with OBD-2 data (my sessions didn’t have a scanner connected), PRs are welcome.

What I learned about AI and reverse engineering

Binary format analysis turns out to be one of the best use cases I’ve found for working with Claude Code:

  • Pattern recognition in hex dumps is right in its wheelhouse. It spotted the PNG-style magic and the record structure faster than I could have.
  • Rapid hypothesis testing is the killer feature. “What if this is cm/s?” takes 2 seconds to validate instead of 20 minutes of scripting.
  • Cross-validation comes naturally. “Compare GPS speed to haversine-derived speed” is one prompt away from a definitive answer.

But the human steering was non-negotiable. Claude tried Unix timestamps first on the GPS data: the dates came back 40 years off. I knew GPS chipsets use their own epoch and suggested trying it. The Z-axis accelerometer reading of ~1000 only means milli-g if you know what 1g looks like at rest. The gyroscope calibration needed physical reality checks against known turn rates.

These tools are fast, but they’ll confidently build garbage if you let them. The RKD project worked because I was in the driving seat (pun intended) the whole time: redirecting wrong hypotheses, knowing what physical values should look like, deciding which unknowns matter and which can be left for later. The 100% branch coverage and byte-for-byte parity between Python and Go aren’t decorative: that’s how you maintain code you didn’t type.