Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

03 — SysEx protocol

SysEx is used for everything that isn’t a single live parameter: identification, reading/writing whole presets, names, system settings, looper/drum/tuner state, and uploads (NAM/IR). Live parameter changes use CCs (02).

Confidence: confirmed = seen on the wire and consistent; inferred = consistent with the captures and the static analysis but not independently proven.

Manufacturer ID and frame types

All vendor SysEx uses the 3-byte manufacturer ID 00 21 7A. Two frame shapes exist:

A. Short identification frames (no command byte)

RequestReplyMeaning
F0 00 21 7A 00 F7F0 00 21 7A 10 <32 B ascii version> <ascii model> 00 00 F7Version: "V1.1.1" (zero-padded to 32 bytes) then "JME-01"
F0 00 21 7A 60 F7F0 00 21 7A 60 <24 B ascii product name> 0A 00 00 00 00 00 00 00 F7Product: "JME STOMPBOX", then 0x0A (10, meaning unknown; maybe the number of user slots)

The pedal also answers the universal identity request F0 7E 7F 06 01 F7 with F0 00 21 7A 10 F7, which is not a standard identity reply. Two unsolicited F0 00 21 7A 10 F7 messages also appeared mid-session; meaning unknown.

B. NEP command frames (everything else)

F0  00 21 7A  70  <cmd>  <dir>  <payload ...>  F7
    \_mfr__/  uid  msgId  direction
  • uid is always 0x70.
  • cmd (msgId) selects the data object (table below).
  • dir:
dirMeaningSent by
00read requesthost
01writehost
02read reply (data)pedal
03write acknowledgepedal
7Eused as cmd = notification (see below)pedal
  • No checksum observed.
  • The official app is strictly request → response. It waits for the 02/03 before sending the next frame. Copy that behaviour; a 1 s timeout per request is sensible (the static analysis shows a 1000 ms timeout and a mutex in the sender).
  • Payload bytes are always ≤ 0x7F (SysEx rule). How 8-bit data is carried depends on the command; see Payload encodings.

Notifications

When state changes on the pedal side for something that has no CC, the pedal sends:

F0 00 21 7A 70 7E 02 <cmd> 02 <payload...> F7

Seen:

<cmd>TriggerPayloadWhat the app does next
0x6Ftuner opened/closed on pedal00 00 00 00 00 00reads 0x6F, writes it back unchanged
0x19tap tempo, BPM knob, drum pattern change, loop recorded6 bytes, same layout as the drum record (BPM at bytes 4–5)nothing (data is inline)
0x0Bpreset saved on the pedal<index> 00 00 00 00 00reads preset <index> (70 0B 00 <index> 00*13)

Command table

cmdObjectRead (dir 0)Write (dir 1)EncodingStatus
0x09User IR slot info (index, CRC, name)70 09 00 <slot>70 09 01 <slot> <packed>slot byte + packedconfirmed
0x0BPreset (by index)70 0B 00 <idx> 00*1370 0B 01 <packed 126 B>packed (whole body)confirmed
0x0CCurrent / edit-buffer preset70 0C 00 00*14?packed, same layout as 0x0B (index field 0)read confirmed
0x0DModule link / chain (static)not seen
0x10User IR upload (data chunks)70 10 01 <packed chunk>packedconfirmed
0x12Preset name70 12 01 <slot?> <16 B ASCII>plain ASCIIconfirmed
0x14System settings70 14 0070 14 01 <packed 20 B>packedconfirmed
0x15Current state70 15 00?plainread confirmed
0x16Delete IR (static)not seen
0x18Looper settings/state70 18 0070 18 01 <5 B>plainconfirmed
0x19Drum70 19 00?plainread confirmed
0x1BBluetooth params (static)not seen
0x58User AMP (NAM) slot: info, data, delete70 58 00 <packed hdr>70 58 01 <packed>packedconfirmed
0x6FTuner70 6F 0070 6F 01 <5 B>plainconfirmed

Write acks are short: 70 <cmd> 03 + a few bytes (often zeros, sometimes an echo of the request header).

Startup handshake (what the official app does on connect, ~320 ms total)

F0 00 21 7A 00 F7                 version
70 14 00                          system settings
70 15 00                          current state
70 19 00                          drum
70 09 00 <0..9>       ×10         user IR slots
F0 00 21 7A 60 F7                 product name
70 58 00 <slot 0..9>  ×10         user AMP (NAM) slots
70 0B 00 00 …                     preset 0
70 0C 00                          edit buffer
70 18 00                          looper
70 0B 00 <1..98> …    ×98         presets 1..98   (99 presets total: P01..P99)

Payload encodings

Three encodings are in use. Which one applies depends on the command.

1. Plain 7-bit bytes

Each byte is a value 0–127. 14-bit values are two bytes, high 7 bits first: value = (hi << 7) | lo. Used by 0x15, 0x18, 0x19, 0x6F, the 0x12 name and notification payloads.

2. Packed 8-in-7 bitstream (“packed”)

8-bit data packed into 7-bit MIDI bytes as one little-endian bitstream: every MIDI data byte contributes its 7 low bits, LSB first, and those bits are concatenated. Regroup into 8-bit bytes LSB first to get the struct. 126 MIDI bytes → 110 struct bytes (126*7/8).

def unpack(midi: bytes) -> bytes:          # tools/decode/unpack.py
    bits = "".join(format(b & 0x7F, "07b")[::-1] for b in midi)
    return bytes(int(bits[i:i+8][::-1], 2) for i in range(0, len(bits) - 7, 8))

def pack(data: bytes) -> bytes:
    bits = "".join(format(b, "08b")[::-1] for b in data)
    bits += "0" * (-len(bits) % 7)
    return bytes(int(bits[i:i+7][::-1], 2) for i in range(0, len(bits), 7))

In Rust, think of it as a bit writer/reader with a 7-bit output width (see 06).

For 0x0B, 0x0C, 0x14, 0x58 and 0x10 the entire body after <dir> is one packed stream, including the header fields. Evidence: the 0x58 slot reads use MIDI bytes 00 00, 00 02, 00 04… for slots 0, 1, 2, which is exactly how slot = 0, 1, 2 at unpacked byte 1 comes out after packing.

3. Mixed: plain index byte + packed record

0x09 (IR slot info) = one plain byte (slot index), then a packed stream starting at the next MIDI byte.

Plain text

0x12 preset name: plain ASCII, one char per byte, zero padded to 16. (Inside packed records, names are just bytes of the struct.)

Per-command payloads

Struct layouts for 0x0B/0x0C (preset), 0x14 (settings), 0x09, 0x58, 0x18, 0x19, 0x6F are in 04-data-structures.md. Upload procedures for 0x58 (NAM) and 0x10 (IR) are in 05-uploads-nam-ir.md.

Captured write sequences

Rename + save-as (app “save” dialog, changing name and number)

→ 70 12 01  44  34 4F 6F 7A 21 00…00      set name "4Ooz!" (first byte 0x44 = the active preset, P69)
← 70 12 03  00*8
→ 70 0B 01  <126 B packed preset, index 0x15>   write full preset to index 21 (P22)
← 70 0B 03  15 00 00 00 00 00 00 00 00 44 01 00 00 00
→ C1 15                                    select the new slot (ch 2 program change)

Settings change (any field)

The app rewrites the whole settings block on every change (dozens of times per second while dragging):

→ 70 14 01 <20 B packed>    ← 70 14 03 00*8

Tuner open/close on the pedal

← 70 7E 02 6F 02 00…           notify
→ 70 6F 00                     read
← 70 6F 02 01 00 0A 00 00 …    (status=1: tuner on)
→ 70 6F 01 01 00 0A 00 00      write back unchanged
← 70 6F 03 01 00 …             ack

Opening the looper panel

→ B1 3B 08                     show LOOP page on pedal
→ 70 18 00   ← 70 18 02 01 32 32 <state> 00 …
→ 70 18 01 01 32 32 <state> 00  ← 70 18 03 01 00 …