Skip to content
roll2own

M25 Protocol

How to talk to wheelchair wheels. It’s not rocket science, but it’s not exactly simple either.

Bluetooth SPP (Serial Port Profile) on channel 6, wrapped in AES-128-CBC encryption. Nothing exotic, but they did add some… creative choices.

  • 0xEF: Every packet starts with this. Exciting.
  • Length: Big-endian, bytes after the header
  • Encrypted IV: 16 bytes, encrypted with ECB (yes, really)
  • Encrypted Data: The actual payload, CBC encrypted
  • CRC-16: Just in case the encryption wasn’t enough

Because 0xEF can appear in data too:

  • EF in payload → EF EF on wire
  • Single EF = packet header
  • Double EF = just the byte 0xEF

Six bytes of header, then whatever data the command needs.

IDDeviceNotes
0x01M25 Wheel (broadcast)Both wheels listen
0x02Left Wheel
0x03Right Wheel
0x04ECS RemoteThe €595 one
0x05Smartphone AppThat’s us now
0x06Knob Remote
0x07USB Service ToolDealer stuff
IDServicePurpose
0x01APP_MGMTDrive modes, assist levels, the fun stuff
0x02BATT_MGMTBattery state
0x03VERSION_MGMTFirmware versions
0x04RTCReal-time clock
0x05STATISTICSObviously stats
0x06MEMORY_MGMTRaw config memory
0x08TANDEMFor paired wheelchairs
IDWhatDirection
0x10WRITE_SYSTEM_MODE→ Wheel
0x20WRITE_DRIVE_MODE→ Wheel
0x21READ_DRIVE_MODE→ Wheel
0x22STATUS_DRIVE_MODE← Wheel
0x30WRITE_REMOTE_SPEED→ Wheel
0x40WRITE_ASSIST_LEVEL→ Wheel
0x41READ_ASSIST_LEVEL→ Wheel
0x42STATUS_ASSIST_LEVEL← Wheel
IDWhat
0x01READ_SOC
0x02STATUS_SOC

The QR code on each wheel contains your AES key. Encoded in their own special way.

  1. 22 characters from a custom 64-character alphabet
  2. Each char = 6 bits (22 × 6 = 132 bits)
  3. Drop the first 4 bits → 128 bits → AES key

Why not just print the hex key? Where’s the fun in that?

  1. Generate random 16-byte IV
  2. Encrypt IV with AES-ECB → Encrypted IV (why? nobody knows)
  3. PKCS7-pad the payload
  4. Encrypt with AES-CBC using the IV
  5. Calculate CRC-16 over the original payload
  6. Assemble: Header + Encrypted IV + Encrypted Data + CRC
  1. Strip header, get length
  2. Decrypt IV with AES-ECB
  3. Decrypt payload with AES-CBC
  4. Remove PKCS7 padding
  5. Verify CRC-16
  6. Parse the SPP data

Polynomial 0x8005, init 0xFFFF. Standard stuff. See m25_protocol.py for the lookup table.