a5d2865eb2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
143 lines
7.4 KiB
Markdown
143 lines
7.4 KiB
Markdown
# 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
|
|
1. Enabled Bluetooth HCI snoop log on a Pixel 10 Pro XL (Android Developer
|
|
Options → Enable Bluetooth HCI snoop log)
|
|
2. Used the official Denon app to perform actions: connect, volume up/down,
|
|
input selection, etc.
|
|
3. Extracted the log via `adb bugreport` (direct `adb pull` is blocked on
|
|
Android 15) and unzipped the btsnoop_hci.log from inside the zip
|
|
4. Parsed the binary BTSnoop v1 file in Python (no external dependencies)
|
|
and identified ACL data packets containing the string "AT" (0x41 0x54)
|
|
5. 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 using `socket.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 | ('U' — label approximate, needs confirmation)
|
|
| 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.Queue` polled with `after(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 `AT` header 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
|
|
1. Test power on/off and mute on/off against the real receiver
|
|
2. Confirm the 0x55 source label (likely "Media Player" or "USB")
|
|
3. Do a third btsnoop capture targeting: sound mode switching, tuner, quick
|
|
select
|
|
4. Add reconnect logic for dropped connections
|
|
5. Build out a more complete and polished application
|