Skip to content
Kira

· 3 min read

The checksum that wasn’t a CRC: decoding the JamJum JP-1 protocol

A reverse-engineering log of a MIDI controller: USB captures, a wrong hypothesis and the missing piece that made the device accept arbitrary per-pad color.

#reverse engineering#midi#usb#python
Leer este artículo en español

The JamJum JP-1 is a MIDI controller with 16 RGB pads, 8 knobs and 6 transport buttons. It works fine, but its official app is limited and the vendor does not publish the protocol. I wanted to batch-edit it, with my own light effects, from the browser. That meant learning to speak its language.

Step 1: listen to the conversation

The device is standard USB-MIDI, so the traffic can be captured with USBPcap and read in Wireshark. The official app sends proprietary SysEx messages shaped like this:

F0 78 32 <3-byte command> <payload> F7

The payload is not made of regular bytes. It is a 7-bit-per-byte, LSB-first bitstream: the low 7 bits of every byte are concatenated. A field can start halfway through one byte and end in the next. Before understanding any value, I had to write a decoder for that stream.

Step 2: change one thing at a time

The method that worked was the boring one: change one parameter in the official app, capture, and diff bit by bit against the previous capture. That produced the map of each pad: channel, type (note, CC, program change), number and velocity range, in 32-bit records.

And at the end of every block, two bytes that changed “for no reason”: the checksum.

Step 3: the wrong hypothesis

A two-byte checksum that changes with everything sounds like a CRC. I tested whether the system was linear over GF(2) —like a CRC— and the test said yes. The problem: I had far too few equations for so many unknowns, and with little data almost any model “fits”. The hypothesis was false, and it took several tests on the device to find out.

The lesson went into the project log: an underdetermined solvability test proves nothing.

Step 4: modular sums

Once I recast it as a weighted modular sum, everything started to close. The pads checksum, for example:

pads_checksum.py
def pads_checksum(pads: list[dict]) -> tuple[int, int]:
s = sum(p["channel"] + 16 * p["type"] + p["note"] + p["min"] + p["max"] for p in pads)
c = (510 - 2 * s) % 512
return c & 0x7F, (c >> 7) & 0x7F # bytes 76 and 77

I verified it against the 63 official blocks I had captured: identical byte for byte. Then the buttons (8175 − 32·Σ) mod 8192 and knobs (15456 − 32·Σ) mod 16384 checksums fell. Same family.

Step 5: color fought back

Color played a trick. A “uniform” model fit my first captures perfectly… because those captures lacked variety: the model fit by aliasing, not because it was right. On the device, it failed.

The way out was designing a new capture meant to break the model: all 16 pads with independent colors, varied brightness and different indices. With that data, a search of the form (K + w·Σ) mod M over several definitions of Σ found the exact answer:

color_checksum.py
def color_checksum(channels: list[int], pad_index: int) -> tuple[int, int]:
"""channels: the 48 8-bit values (16 pads × G, R, B)."""
s = sum(channels) + pad_index # ← the missing piece
chk = (2720 - 32 * s) % 8192
return chk & 0x7F, (chk >> 7) & 0x7F # bytes 64 and 65

The missing piece was adding the pad index. Without it, two blocks with the same colors but different indices produced the same result, which could never be reconciled with the captures.

Result: 384 out of 384 blocks reproduced byte for byte, and an arbitrary rainbow —16 different colors generated by my code— accepted by the device.

Takeaways

  1. Design the capture that can kill your hypothesis, not the one that confirms it.
  2. Verify on the hardware, not just on the computer. The device is the only judge.
  3. Test the layer people use. I validated the engine from the console while the UI had a different bug; now I test end to end.

Today the app (JamJum Studio, built on Web MIDI) reads the configuration on connect, edits pads, knobs, buttons and color, and adds light effects the official app lacks. The full case is in projects.