Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user