Compiled from public reverse engineering research. All information is derived from:
- https://blraaz.me/reverse-engineering/2021/08/29/bluetooth-reverse-engineering.html
- https://blraaz.me/reverse-engineering/2021/10/04/pax-protocol-electric-boogaloo.html
- https://github.com/tristanseifert/pax-controller-test
- https://github.com/evertonstz/pax-romana
| UUID | Description |
|---|---|
0x180A |
Device Information Service |
0x2A25 |
Serial Number String — critical for key derivation |
0x2A24 |
Model Number String |
0x2A26 |
Firmware Revision String |
0x2A29 |
Manufacturer Name String |
| UUID | Description | Properties |
|---|---|---|
8E320200-64D2-11E6-BDF4-0800200C9A66 |
PAX Service | — |
8E320201-64D2-11E6-BDF4-0800200C9A66 |
Read characteristic | Read |
8E320202-64D2-11E6-BDF4-0800200C9A66 |
Write characteristic | Write |
8E320203-64D2-11E6-BDF4-0800200C9A66 |
Notify characteristic | Notify |
The 8E32 prefix and 64D2-11E6-BDF4-0800200C9A66 suffix are shared across all PAX service UUIDs.
All packets exchanged on the read/write characteristics are 32 bytes:
[ ciphertext: 16 bytes ][ IV: 16 bytes ]
- Encryption: AES-128 in OFB mode
- IV: the last 16 bytes of the 32-byte packet
- The IV is randomly generated by the sender for each packet
Decrypted plaintext structure (16 bytes):
[ message type: 1 byte ][ payload: up to 15 bytes ][ zero padding ]
All multi-byte values are little-endian.
- Read the serial number from characteristic
0x2A25(8 ASCII characters, e.g.A1B2C3D4) - Concatenate it with itself:
A1B2C3D4A1B2C3D4→ 16 characters - Encode as UTF-8 → 16 bytes
- Encrypt with the shared key using AES-128 ECB mode
- The result is the 16-byte session key
Shared key (publicly documented in multiple open-source projects):
8A EC F0 AB DE F0 4E 30 B8 00 A0 D7 64 C0 9E 65
This key is hardcoded in all versions of the PAX mobile app and is not a secret — it is documented in multiple public security research publications.
- Subscribe to the Notify characteristic (
8E320203) - When data is available, the device sends a notification on
8E320203 - The notification value itself is not the data — it signals readiness only (may contain first byte of pending data, but this is discarded)
- In response to a notification, the host reads the Read characteristic (
8E320201) - The 32-byte read value is then decrypted as described above
| Value | Name | Direction | Payload |
|---|---|---|---|
0x01 |
ActualTemp |
Device → Host | 2 bytes LE uint16: temperature in °C × 10 |
0x02 |
HeaterSetPoint |
Both | 2 bytes LE uint16: temperature in °C × 10 |
0x03 |
Battery |
Device → Host | 1 byte: 0–100% |
0x04 |
Usage |
Device → Host | Unknown format |
0x05 |
UsageLimit |
Both | Unknown |
0x06 |
LockStatus |
Device → Host | 1 byte: 0 = unlocked, 1 = locked |
0x07 |
ChargeStatus |
Device → Host | 1 byte: charge state (not fully decoded) |
0x08 |
PodInserted |
Device → Host | 1 byte (PAX Era only) |
0x09 |
Time |
Both | Unknown format |
0x0A |
DisplayName |
Both | 1 byte length + UTF-8 string |
0x11 |
HeaterRanges |
Device → Host | Unknown format |
0x13 |
DynamicMode |
Both | 1 byte mode ID (PAX 3) |
0x14 |
ColorTheme |
Both | Unknown |
0x15 |
Brightness |
Both | 1 byte (0–100?) |
0x17 |
HapticMode |
Both | 1 byte mode ID |
0x18 |
SupportedAttributes |
Device → Host | 8 bytes LE uint64 bitfield; bit N set = attribute N supported |
0x19 |
HeatingParams |
Both | Unknown |
0x1B |
UiMode |
Both | 1 byte |
0x1C |
ShellColor |
Both | Unknown |
0x1E |
LowSoCMode |
Both | Unknown |
0x1F |
CurrentTargetTemp |
Device → Host | 2 bytes LE uint16: PID target in °C × 10 (PAX 3) |
0x20 |
HeatingState |
Device → Host | 1 byte; see table below (PAX 3) |
0x28 |
Haptics |
Both | Unknown |
0xFE |
StatusUpdate |
Host → Device | 8 bytes LE uint64 bitfield; request device send current values of indicated attributes |
| Byte | State |
|---|---|
0x00 |
Off |
0x01 |
Standby |
0x02 |
Heating |
0x03 |
Ready (at temperature) |
0x05 |
Cooling |
0x08 |
Boost mode |
encoded_value = celsius * 10
celsius = encoded_value / 10.0
Example: 180°C → 0x0708 (1800 decimal, little-endian: 08 07)
To request current values, send a packet with type 0xFE and an 8-byte little-endian bitfield payload where each bit corresponds to the attribute number you want:
// Request battery (bit 3) and actual temp (bit 1)
var bitfield: UInt64 = 0
bitfield |= (1 << 3) // battery
bitfield |= (1 << 1) // actual tempThe device will respond with individual notification packets for each requested attribute.
| Feature | Status |
|---|---|
| Service discovery | ✅ |
| Serial number read → key derivation | ✅ |
| Notify subscription | ✅ |
| Full status request (0xFE) | ✅ |
| ActualTemp (0x01) decode | ✅ |
| HeaterSetPoint read (0x02) | ✅ |
| HeaterSetPoint write (0x02) | ✅ |
| Battery (0x03) | ✅ |
| HeatingState (0x20) | ✅ |
| LockStatus (0x06) | ✅ |
| CurrentTargetTemp (0x1F) | ✅ |
| DisplayName (0x0A) | ✅ |
| Firmware / model from Device Info | ✅ |
-
Maximum packet length: All tested packets are 16 bytes plaintext (32 bytes encrypted). Whether longer payloads are supported is not confirmed.
-
ChargeStatus (0x07): The exact byte encoding is not publicly documented. The app ignores this value.
-
HeaterRanges (0x11): Format unknown. Possibly encodes min/max allowed temperature bounds.
-
DynamicMode (0x13): Known PAX 3 values are Standard (
0x00), Boost (0x01), Efficiency (0x02), Stealth (0x03), and Flavor (0x04). Their exact heating algorithms remain device-controlled and are not fully documented. -
SupportedAttributes (0x18): Should be queried first to know which attributes a device supports, but has been omitted from the initial implementation for simplicity.
-
Pax Era / Era Pro compatibility: Most message types are documented as "All devices" but this app has only been designed around PAX 3. Era-specific messages (PodInserted, etc.) are parsed but not displayed.
-
Endianness of HeatingState: Assumed 1-byte, no multi-byte values documented. Some unknown states may exist between the known values.
-
Write response timing: Whether the device requires a delay between consecutive write commands is not documented. This app writes one packet at a time and relies on CoreBluetooth's
.withResponsewrite type. -
Session re-keying: Whether the session key changes between connections (it shouldn't, as it is derived purely from a static serial number) is unconfirmed.
-
Firmware versions: Protocol may have changed between firmware versions. All documentation is based on app analysis circa 2021.