Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.4 KiB
Denon AVR-S540BT Bluetooth Remote — Project Context
What We're Building
A custom Python application to control the Denon AVR-S540BT AV receiver over Bluetooth, replicating the functionality of the official "Denon 500 Series Remote" Android app without relying on it.
How We Got Here
Discovery
The AVR-S540BT has no network (no Ethernet, no WiFi). The only remote control interface is Bluetooth, used by Denon's own "Denon 500 Series Remote" app. Denon does not publish the Bluetooth control protocol.
Reverse Engineering Method
- Enabled Bluetooth HCI snoop log on a Pixel 10 Pro XL (Android Developer Options → Enable Bluetooth HCI snoop log)
- Used the official Denon app to perform actions: connect, volume up/down, input selection, etc.
- Extracted the log via
adb bugreport(directadb pullis blocked on Android 15) and unzipped the btsnoop_hci.log from inside the zip - Parsed the binary BTSnoop v1 file in Python (no external dependencies) and identified ACL data packets containing the string "AT" (0x41 0x54)
- Decoded the packet structure
Protocol Details
- Transport: Bluetooth Classic, RFCOMM (Serial Port Profile)
- Channel: 2 (service name "BT SPP 2", confirmed via SDP in the log)
- Framing: Each packet starts with bytes
41 54("AT"), followed by binary command bytes. The trailing byte in raw HCI is an RFCOMM FCS (Frame Check Sequence) added by the kernel — NOT part of the application payload. When usingsocket.AF_BLUETOOTH / BTPROTO_RFCOMM, the kernel handles framing automatically; you just read/write raw application data.
Decoded Commands
Phone → AVR (commands)
| Action | Bytes (hex) | Notes |
|---|---|---|
| Volume Up | 41 54 07 00 00 00 |
|
| Volume Down | 41 54 07 01 00 00 |
|
| Mute On | 41 54 07 1D 01 01 FE |
Discrete on |
| Mute Off | 41 54 07 1D 01 00 FF |
Discrete off |
| Power Off | 41 54 00 0A 01 00 FF |
|
| Power On | 41 54 00 0A 01 01 FE |
|
| Get Input Sources | 41 54 00 04 00 00 |
|
| Get Status | 41 54 00 06 00 00 |
|
| Select Input | 41 54 00 07 01 [src] [chk] |
chk = 0xFF XOR src |
| Mute Toggle* | 41 54 07 25 01 20 DF |
Used in handshake, not user |
AVR → Phone (responses)
| Response | Bytes (hex) | Notes |
|---|---|---|
| Volume Report | 41 54 07 02 03 C5 [vol] 00 [chk] |
vol/2 = display value, chk = 0xFF-vol+1 |
| Mute State | 41 54 07 1D 01 [state] [chk] |
state: 01=muted, 00=unmuted |
| Sound Mode | 41 54 07 1E 15 00 [20 bytes text] [chk] |
ASCII, space-padded, 27 bytes total |
| Power Ack | 41 54 07 0B 01 00 FF |
Sent by AVR on power off |
| Input Sources | 41 54 00 04 0A 07 40 81 55 82 52 53 54 56 57 B1 |
0x81/0x82 are category separators |
| Current Input | 41 54 00 07 02 [src] 00 [chk] |
|
| Power Echo | 41 54 00 0A 01 [state] [chk] |
AVR echoes power state back |
| Status Keepalive | 41 54 00 0B 01 07 F8 |
Sent by AVR after power on |
| Device Name | 41 54 00 0E 0A 00 [name bytes] [chk] |
Returned " DENON AV", 16 bytes total |
| Status Response | 41 54 00 06 45 ... [75 bytes] |
Full status dump |
Known Input Source Byte Codes
These were observed in the log. Exact label↔code mapping is approximate and should be confirmed by testing:
| Byte | Label |
|---|---|
| 0x40 | Bluetooth |
| 0x55 | Media Player |
| 0x52 | CBL/SAT |
| 0x53 | DVD |
| 0x54 | Blu-ray |
| 0x56 | Game |
| 0x57 | AUX |
Second Capture (Session 2)
Actions performed: volume 20.5→21, mute on, mute off, volume down, mute on, mute off, power off, power on, power off, power on.
New findings:
- Mute is discrete on/off (07 1D), not a toggle. The 07 25 command seen in the first capture is part of the connection handshake, not user mute.
- Power uses 00 0A with 01 00 = off, 01 01 = on.
- After power on, the AVR sends a status keepalive (00 0B 01 07) and the Denon app replays the mute-toggle handshake (07 25) before resuming.
- Volume display mapping: raw byte / 2 = display value (e.g., 0x29=41 → display "20.5", 0x2a=42 → display "21").
- Source byte 0x55 ("Media Player"?) discovered in sources list.
- 0x81 / 0x82 in the sources response are category separators, not sources.
Commands Not Yet Captured
The following still need to be reverse engineered:
- Individual sound mode selection (only the status report was seen)
- Tuner controls (band, tune up/down, preset)
- Quick Select buttons
Current Codebase
denon_remote.py
A Python 3 tkinter application (stdlib + optional python3-dbus for device auto-detection).
Key design decisions:
- Uses
socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM(Linux built-in, no PyBluez needed) - Receive loop runs in a background daemon thread
- All UI updates routed through a
queue.Queuepolled withafter(50,...)to keep tkinter thread-safe - Packet length detection is best-effort heuristic based on known packet types;
unknown packets fall back to reading until the next
ATheader or 32 bytes - Input source buttons are rendered dynamically from the AVR's own source list response rather than being hard-coded
- Auto-detects paired Denon devices via BlueZ D-Bus (falls back gracefully if python3-dbus is unavailable)
- Power on/off and discrete mute on/off commands implemented
- Mute state tracked and reflected in UI (button changes to red "MUTED")
- Volume displayed as the AVR's display value (raw_byte / 2)
Known limitations / things to improve:
- Packet length detection covers all observed packet types but unknown types may still be mis-framed
- Input source label mapping is approximate (especially 0x55 "Media Player")
- No reconnect logic if the connection drops
- Sound mode selection not implemented (only displays current mode)
- The UI aesthetic uses Courier as a monospace font — works everywhere but could use a proper monospace like JetBrains Mono or similar if available
Environment
- Dev machine: Linux (Ubuntu/Debian), user ckoch on host Shinobu
- Receiver: Denon AVR-S540BT
- Test phone: Pixel 10 Pro XL (Android 15)
- Tools used: adb (34.0.4), Wireshark 4.2.2, Python 3.12
Next Steps
- Test power on/off and mute on/off against the real receiver
- Confirm the 0x55 source label (likely "Media Player" or "USB")
- Do a third btsnoop capture targeting: sound mode switching, tuner, quick select
- Add reconnect logic for dropped connections
- Build out a more complete and polished application