Reverse Engineering the Concept2 Utility to Update PM5 Firmware on Linux

Concept2’s PM5 monitor firmware can be updated via USB, but the official tool — Concept2 Utility — only runs on Windows. No Linux support, no documented protocol. I wanted to update my PM5 from my Debian machine, so I reverse engineered the Windows binary to figure out exactly what it does. I used the script to update my PM5 from v217.037 to v217.067.

The Reverse Engineering Setup

I used Ghidra as the disassembly and decompilation platform, combined with the Pi coding agent and a custom Ghidra skill I’ve been building. The skill lets Pi drive Ghidra directly — running scripts, querying the API, renaming functions, following cross-references — so the RE work becomes a conversation rather than manual click-through.

The target binary was Concept2 Utility.exe v7.18.00: a 64-bit Windows PE, compiled with MSVC, built on Qt 6. No PDB symbols, but MSVC RTTI was fully intact, which meant Ghidra’s auto-analysis recovered all the class names and vtables. Qt stores signal/slot prototypes as plain C strings with a numeric prefix, so grepping the string table for ^[12][a-zA-Z] immediately surfaced most of the interesting class and method names.

With that as a starting point, Pi was able to trace the full firmware update flow: from the initial API request through to the exact bytes written on the USB drive.

What the Utility Actually Does

The firmware update path is straightforward once you have the function names:

1. Firmware manifest API

The Utility fetches a JSON manifest from Concept2’s own API (FirmwareInfoJSON::requestLatest, 0x1401a50b0):

GET https://tech.concept2.com/api/firmware/latest?client=utility-windows
Authorization: Basic <base64(hardcoded credentials)>

The response is a data array where each element describes one firmware release — version, target hardware variant, status (public or internal), and a list of .7z download URLs.

2. USB drive validation

Before writing anything, C2UtilityMainWindow__removableDriveCandidateStatusChanged (0x14009ed00) checks three things via QStorageInfo:

  • Filesystem type contains "FAT" or "msdos" (FAT16 or FAT32)
  • Partition table type contains "MBR" — GPT is explicitly rejected
  • Bus type contains "USB"

3. Writing the USB drive

updateFlashDrive (0x140173740) iterates all applicable PM5 hardware variants, copies each .7z archive to Concept2/Firmware/ on the USB, then calls PmUnzip::extract to decompress the .bin files into the same flat directory. Multiple hardware variants (pm5_, pm5v2_, pm5v3_, pm5v5_, pm5v6_, etc.) all coexist in the same directory with no conflicts — each uses a distinct filename prefix.

The resulting layout looks like this:

<USB_ROOT>/
└── Concept2/
    └── Firmware/
        ├── pm5v6_allbin_secure_R459B037.7z
        ├── pm5v6_bundle.bin
        ├── pm5v6_appint.bin
        ├── pm5v6_appext.bin
        ├── pm5v6_ldrint.bin
        ├── pm5v6_lupint.bin
        ├── pm5v6_font.bin
        ├── pm5v6_screen.bin
        ├── pm5v6_antbleapp.bin
        ├── pm5v6_antbleldr.bin
        └── … (other hardware variants alongside)

The PM5 scans this directory, finds the *_bundle.bin matching its own hardware revision, reads the version from the bundle header, and prompts for confirmation before flashing.

The Script

All of the above is implemented in c2fw-updater: a single Python script with no dependencies beyond the standard library and p7zip.

Install p7zip:

sudo apt install p7zip-full

Format a USB drive and run the script:

# Format (replace /dev/sdX with your device)
sudo parted /dev/sdX mklabel msdos
sudo parted /dev/sdX mkpart primary fat32 1MiB 100%
sudo mkfs.fat -F32 -n C2FIRMWARE /dev/sdX1
sudo mount /dev/sdX1 /mnt/usb

# Download firmware and write to USB
python3 c2fw.py --usb /mnt/usb

sudo umount /mnt/usb

Then on the PM5: MENU → More Options → Utilities → Update PM5 Firmware, insert the USB, and follow the prompts.

By default the script downloads firmware for the rower PM5. To include other machines:

# All machine types (rower, SkiErg, BikeErg, Strength Erg)
python3 c2fw.py --usb /mnt/usb --all-machines

# SkiErg only
python3 c2fw.py --machine skierg --usb /mnt/usb

# List everything available, including beta firmware
python3 c2fw.py --list --all-machines --beta

The script caches downloaded archives locally (in ./concept2_firmware/ by default), so re-running to refresh the USB is fast if nothing has changed upstream.

Limitations & Known Issues

The script doesn’t create the entire USB layout. Specifically, The Concept2/Logbook/ directory is not created. The PM5 will report that there is an issue with the drive and offer to repair it. Performing the repair creates the necssary directories and allows to perform the firmware update. I hope to address that in a future update.

Running Concept2 Utility in a VM

I recently found myself needing to transfer some old workout data from my Concept2 PM3 and its associated Logcard. However, as I didn’t have easy access to a Windows machine, and Concept2 only provides their utility software for Windows and Mac, I was compelled to run it in a Windows VM.

Upon connecting the PM3 to my computer and redirecting the USB device to the VM, I encountered an issue: the Concept2 Utility failed to recognize the connected PM3. In an attempt to resolve this, I downgraded the Concept2 Utility to an older version, 6.54. This version did recognize the PM3, but it still failed to recognize the Logcard.

The solution I found was to add the PM3 as a USB host device, rather than using USB redirection. This can be accomplished via the VM’s hardware details page by selecting Add Hardware -> USB Host Device, or by using the following XML configuration:

<hostdev mode="subsystem" type="usb" managed="yes">
  <source>
    <vendor id="0x17a4"/>
    <product id="0x0001"/>
  </source>
  <address type="usb" bus="0" port="3"/>
</hostdev>

The Vendor ID/Product ID (VID/PID) shown above corresponds to the PM3’s VID/PID. If you’re using a different monitor, you may need to adjust these values accordingly.

For reference, here is an example of how the PM3’s VID/PID appears:

Bus 003 Device 004: ID 17a4:0001 Concept2 Performance Monitor 3

Additional details about the setup include:

  • Windows 10 VM
  • QEMU/KVM virtualization via virt-manager
  • Concept2 Utility version 7.14