Files
denon-bluetooth/PROJECT_CONTEXT.md
ckoch a5d2865eb2 Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:59:47 -04:00

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

  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
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