# LEDDMX Protocol Reference

## ⚠️ STATUS NOTE (as of v1.1.0)

The **9-byte Variant B (`7B...BF`) is the definitive LEDDMX-03 protocol**, confirmed by
decompiling the `com.ledlamp` APK (v4.3.5) and reading the actual `sendData(new int[]{...})`
calls in `com/home/net/NetConnectBle.java`. Use this as the primary source of truth.

The "3-byte" section at the bottom of this file is **keepalive noise**, NOT the protocol.
It was observed in btsnoop captures (where `00 7B FF` repeats every 1-2s while the app is
foregrounded) but cross-referencing with the decompiled source code showed those 3 bytes
are a heartbeat, not a control command. The real ON/OFF/color/brightness commands are
all 9-byte writes that just happen to be drowned out by the keepalive traffic in raw
btsnoop logs.

The 9-byte Variant A (`7E...EF`) is unverified for LEDDMX-03 — kept here for reference
because it came from an HA user report. Try Variant B first.

---

## Source

Decompiled from `com.ledlamp` v4.3.5 via `jadx`. Key class: `com.home.net.NetConnectBle`.
Search for `sendData(new int[]{` in that file — every control command the app can send
appears as a literal `int[]` near the function that calls it.

## Device Identification

- **App:** `com.ledlamp` (Google Play)
- **BLE names:** `LEDDMX-00-XXXX`, `LEDDMX-02-XXXX`, `LEDDMX-03-XXXX`, `LEDDMX-04-XXXX`
- **UUIDs:** Service `0000ffe0-...`, Write `0000ffe1-...`, Read `0000ffe2-...`
- **Addressable strips** (WS2812/SK6812), not simple RGB — `SETPIX` default 200

## Protocol Variant B: 7B...BF (VERIFIED for LEDDMX-03)

From `com/home/net/NetConnectBle.java` — `sendData(new int[]{...})` literals:

```
Power ON:   [0x7B, 0xFF, 0x04, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF]
Power OFF:  [0x7B, 0x04, 0x04, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF]
Color RGB:  [0x7B, 0xFF, 0x07, R,     G,     B,     0xFF, 0xFF, 0xBF]
Brightness: [0x7B, 0xFF, 0x01, SC,    PC,    0x01,  0xFF, 0xFF, 0xBF]
            (SC = brightness * 32 / 100, PC = brightness 0-100)
Effect:     [0x7B, i, 0x12, i2, i3, i4, i5, i6, 0xBF]  (setModeCycle)
            or [0x7B, 0xFF, 0x12, mode, 0x03, 0xFF, 0xFF, 0xFF, 0xBF]  (preset)
Speed:      [0x7B, 0xFF, 0x02, v, 0xFF, 0x01, 0xFF, 0xFF, 0xBF]
Direction:  [0x7B, 0xFF, 0x0D, v, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF]
Music EQ:   [0x7B, 0xFF, 0x0B, v, 0x01, 0xFF, 0xFF, 0xFF, 0xBF]
Color warm: [0x7B, 0x10, v, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF]
Subarea:    [0x7B, 0xFF, 0x14, v, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF]
Custom cyc: [0x7B, 0xFF, 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF]
Config:     [0x7B, 0xFF, 0x0A, a, b, c, 0xFF, 0xFF, 0xBF]
Smart br:   [0x7B, 0xFF, 0x08, i2, (i*32/100), i, 0xFF, 0xFF, 0xBF]
```

### Per-Pixel Writes (Addressable strips)

From `setCar02Rgb(int i, int i2, int i3, int i4)`:
```
[0x7B, 0x07, pixel_idx_1based, R, G, B, 0x00, 0xFF, 0xBF]
```

From `setCar02CustomRgb(int i, int i2, int i3, int i4, int i5, int i6)`:
```
[0x7B, 0x07, i, i2, i3, i4, i5, i6, 0xBF]
```

The first byte after `0x7B` is the **sub-command**, position 0x07 = per-pixel RGB.
`setCar02SetRgb` shows the cleaner form: `[7B, 07, pos, R, G, B, 0, FF, BF]`.

## Protocol Variant A: 7E...EF (UNVERIFIED for LEDDMX-03)

From HA issue #105338 (LEDDMX-03 with com.ledlamp) — user-reported working on one device:

```
Power ON:   7E FF 04 01 FF FF FF FF EF
Power OFF:  7E FF 04 00 FF FF FF FF EF
Set Color:  7E FF 05 03 R  G  B  FF  EF
Brightness: 7E FF 01 BB FF FF FF FF EF  (BB = 0-100 scaled to 0-32)
Effect:     7E FF 03 EE FF FF FF FF EF  (EE = pattern index 0-210)
Speed:      7E FF 02 SS FF FF FF FF EF
```

From ESPHome virtual light (SSDBluetoothVirtualLight namespace):
```cpp
turnOnArray  = {126, 255, 4, 1, 255, 255, 255, 255, 239}  // 7E FF 04 01 ...
turnOffArray = {126, 255, 4, 0, 255, 255, 255, 255, 239}  // 7E FF 04 00 ...
noColorArray = {126, 255, 5, 3, 0,   0,   0,   255, 239}  // 7E FF 05 03 00 00 00 FF EF
colorArray   = {126, 255, 5, 3, R,   G,   B,   255, 239}  // 7E FF 05 03 R G B FF EF
```

## Pattern List (200+ built-in effects)

Patterns selected via command 0x12 (Variant B) or 0x03 (Variant A) with index 0-210.
Categories:
- Forward/Backward Dreaming, Trailing, Streaming, Run
- Open/Close Curtain
- Hop, Strobe, Gradual, Horse Race, Swab
- Color schemes: RD, GN, BU, YE, CN, VT, WH, BK, 7 Colors, RGB Group, YCV Group

## RGB Color Order

The app has an "RGB Sort" setting. LEDDMX-03 default is **GRB**.
When order=GRB, the physical wiring is Green-Red-Blue.
To send a "red" color to the user, swap R↔G in the packet.

Mapping functions:
- RGB: (r,g,b) → [r,g,b]
- GRB: (r,g,b) → [g,r,b]   ← default
- GBR: (r,g,b) → [g,b,r]
- BRG: (r,g,b) → [b,r,g]
- BGR: (r,g,b) → [b,g,r]

## Timing Protocol (from Variant B)

```
Set timing:    [0x8B, packed_day_pos, mode, weekdays, hour, minute, cur_hour, cur_minute, 0xBF]
Terminate:     [0x7B, 0xFF, 0x10, current_day, list_size, 0xFF, cur_hour, cur_minute, 0xBF]
```

Day packing: upper 4 bits = day(1=Mon..7=Sun), lower 4 bits = list position.
Weekdays: bitmask, bit 0=Mon..bit 6=Sun.

---

# ⚠️ DEPRECATED: LEDDMX-03 "3-byte" Format (Keepalive Noise, NOT the Protocol)

This section is kept for historical context. The 3-byte format was inferred from a btsnoop
capture where `00 7B FF` appeared repeatedly. **It turned out to be a heartbeat/keepalive
the app sends while foregrounded, not a control command.** The actual control commands
(ON, OFF, color, brightness) are 9-byte writes, see the Variant B section above.

```
[0x00, header, param]  where header ∈ {0x7B, 0x8B}
```

Observed in btsnoop:
- `00 7B FF` (keepalive, every 1-2s)
- `00 8B 10/11/FF` (timing config bytes — NOT control commands, app-side scheduling)

**Why this confused earlier analysis:** The btsnoop log was dominated by keepalive bytes
because the actual ON/OFF/color commands happen once per user action (sparse) while the
keepalive fires constantly. When parsing the log, the repeated `00 7B FF` looked like
"the protocol", but the decompiled `com.home.net.NetConnectBle.sendData()` calls show
the real commands are 9-byte.

## Reverse Engineering via APK Decompile (the fast path)

```bash
# 1. Pull the APK from the device
APK_PATH=$(adb -s <phone_ip:port> shell pm path com.ledlamp | sed 's/package://' | tr -d '\r')
adb -s <phone_ip:port> pull "$APK_PATH" /tmp/app.apk

# 2. Install jadx
wget -q https://github.com/skylot/jadx/releases/download/v1.5.0/jadx-1.5.0.zip -O /tmp/jadx.zip
unzip -q -o /tmp/jadx.zip -d /tmp/jadx
chmod +x /tmp/jadx/bin/jadx

# 3. Decompile
/tmp/jadx/bin/jadx -d /tmp/decompiled --no-res /tmp/app.apk

# 4. Find LED command definitions
grep -rn "sendData(new int\[\]" /tmp/decompiled/sources/com/ | head -50

# 5. Get context — find which function each command belongs to
# In /tmp/decompiled/sources/com/home/net/NetConnectBle.java, search for the function
# name preceding each `sendData(new int[]{123, ...})` call.
```

The decompiled source uses `int` literals (e.g. `123` for `0x7B`), so the grep output
will be in decimal. Convert with `printf '0x%X\n' 123` if needed.

## When to Use Each Method

| Situation | Method | Why |
|---|---|---|
| Have the app package, want all commands at once | **APK decompile** | Definitive, fast, no app interaction needed |
| App is closed-source / obfuscated beyond `jadx` | HCI snoop | Only option if source is unreadable |
| Need to see what the *device* sends back (notifications) | HCI snoop | Read direction needs real capture |
| Verifying a PWA that you think works | HCI snoop + APK | Cross-check both |
| Need timing of when commands fire (debugging "fires twice" bugs) | HCI snoop | Decompiled source loses timing info |

## Practical Notes for Building a PWA

1. **Send 9-byte writes via `writeValueWithoutResponse`** for low latency. Web Bluetooth
   caps at ~30 writes/sec.

2. **For per-pixel snake animations**, only update stripe boundaries. When shifting a
   horizontal stripe pattern by 1 pixel, only ~6 pixels change color. Updating all 200
   pixels per frame takes ~6.5s; updating 6 takes ~200ms.

3. **The `0xFF` placeholder bytes (bytes 4-7 of most commands) are** likely padding or
   reserved fields — they don't change with parameters but the device expects them.

4. **Color format: positions 3-5 of color command are R, G, B** (0-255 each, straight
   8-bit values, not GRB-encoded). RGB color ORDER (GRB vs RGB) is a separate software-
   level concept — some devices interpret the physical channels in a different order.

5. **For ON/OFF**, the second byte (`0xFF` for ON, `0x04` for OFF) is a sub-device
   identifier. `0xFF` = broadcast to all subdevices / main group. `0x04` = the "off"
   variant. The other bytes that differ between ON and OFF: byte 3 is `0x01` (on) vs
   `0x00` (off). Everything else is the same.
