- Shell 58%
- Python 31%
- C 11%
hisense-bt-fix #
Everything needed to make a Hisense AX3100G soundbar (and likely similar Hisense/ErP-compliant soundbars) work reliably as an A2DP Bluetooth speaker on a fresh Linux install — developed on an Orange Pi Zero 3W (aic8800 UART BT, BlueZ 5.82, PulseAudio 17), but nothing here is board-specific.
The soundbar has two independent firmware quirks. macOS happens to avoid both, so it "just works" from a Mac; stock Linux hits both.
Quirk 1 — silent audio: oversized A2DP packets are dropped #
The soundbar's L2CAP Configure Request carries no MTU, which means the default of 672 bytes. The Linux kernel nevertheless negotiates an outgoing MTU of 1021, so PulseAudio happily sends ~965-byte media packets. The soundbar silently discards them: the link stays up, the stream shows "active", every packet is ACKed — and no sound comes out.
Fix: btmtu.c — a tiny LD_PRELOAD shim on bluetoothd that caps the
MTU reported by getsockopt() at 672 (override with env BT_MTU_CAP).
PulseAudio sizes its packets from bluetoothd's Acquire() reply, so capping it
there fixes every audio client at once. (PulseAudio's autodetect_mtu option
does not help — it only affects HFP/SCO.)
Installed as /usr/local/lib/btmtu.so + systemd drop-in
bluetooth.service.d/btmtu.conf. Note: bluetooth.service runs with
ProtectHome=true, so the .so must live outside /home.
Quirk 2 — random shutdown mid-music: AVRCP-inactivity auto-standby #
The soundbar's mandatory ~15-minute ErP auto-power-off is an inactivity timer that ignores the audio entirely. Empirically (measured on repeated sessions, HCI disconnect reason 3 = remote hung up, LED dark):
- real audio at −12 dBFS peaks does NOT reset it — it powers off mid-music, exactly 15m03s after connecting;
- inaudible pilot tones (19 kHz / 25 Hz at ~−20 dBFS) do NOT reset it;
- a static AVRCP
PlaybackStatus: Playingdoes NOT reliably reset it; - what a Mac/phone produces naturally — an AVRCP player whose position advances and tracks change, plus occasional volume commands — DOES.
Fix (two cooperating services):
bin/mpris-keepalive.py— fake MPRIS player (org.mpris.MediaPlayer2.btkeepalive) reportingPlayingwith a position that advances every 15 s and a track change every 4 min.mpris-proxy(ships with BlueZ, must run in the user session) forwards it tobluetoothd, whose AVRCP target sends the resulting notifications to the soundbar.bin/bt-volnudge— every 4 min sends AVRCP passthroughVolumeDown+VolumeUp(net zero, like tapping the remote). This soundbar has no AVRCP absolute-volume support, so passthrough keys are the only volume channel.
With both active (plus the optional pilot tones below) a session survived 75+ minutes; without them every session died in 15–53 min.
Optional: bin/bt-pilot keeps inaudible 19 kHz + 25 Hz sine tones
(PulseAudio module-sine) mixed into the sink. Level-based standby was
disproven for this model, so these are likely unnecessary — kept as an option
for soundbars whose detector is level-based. Disable with
systemctl --user disable --now bt-pilot.
Caveat: AVRCP only helps when its control channel actually connects.
This soundbar opens AVCTP at the same instant as BlueZ and the collision
sometimes kills the channel (visible as MediaControl1.Connected: false).
Sessions without AVRCP may still hit the 15-min timer. Watch it with
bin/bt-watch (logs avrcp=true/false every 10 s).
Contents #
| file | purpose |
|---|---|
btmtu.c |
LD_PRELOAD MTU-cap shim source (quirk 1) |
systemd/btmtu.conf |
bluetooth.service drop-in loading the shim |
bin/mpris-keepalive.py |
live fake MPRIS player: Playing + advancing position + track changes (quirk 2) |
systemd/mpris-keepalive.service |
user unit for the keepalive |
bin/bt-volnudge + systemd/bt-volnudge.service |
4-min AVRCP passthrough volume nudge (quirk 2) |
bin/bt-pilot + systemd/bt-pilot.service |
optional inaudible pilot tones |
bin/bt-watch + systemd/bt-watch.service |
diagnostic logger: connect/disconnect events, AVRCP state, link quality, audio level → ~/.local/share/bt-watch.log |
bin/bt-test |
play left/right/both channel test tones on the soundbar |
install.sh |
builds + installs all of the above |
Install on a fresh system #
Requirements: bluez (with mpris-proxy), pulseaudio (or PipeWire's
libspa-bluez* — untested), gcc, python3-dbus, python3-gi.
./install.sh AC:B1:EE:D7:6F:1E # your soundbar's MAC (bluetoothctl devices)
The script uses sudo for the system half (shim + bluetooth restart) and
installs the user half (keepalive, watcher, mpris-proxy) as your user.
Then pair once:
bluetoothctl
> scan on # power the soundbar on, wait for it to appear
> pair AC:B1:EE:D7:6F:1E
> trust AC:B1:EE:D7:6F:1E
> connect AC:B1:EE:D7:6F:1E
Verify #
journalctl -u bluetooth | grep btmtu # "capping omtu 1021->672" on connect
bt-test both # left/right test tones are audible
busctl --system get-property org.bluez \
/org/bluez/hci0/dev_<MAC_with_underscores> org.bluez.MediaControl1 Connected
tail -f ~/.local/share/bt-watch.log # conn=yes avrcp=true sink=RUNNING ...
A session should now survive well past the 15-minute mark while playing.
Debugging a drop #
Enable BlueZ debug to capture the HCI disconnect reason (1 = link loss, 2 = we hung up, 3 = the soundbar hung up):
sudo sh -c 'printf "[Service]\nExecStart=\nExecStart=/usr/libexec/bluetooth/bluetoothd -d\n" \
> /etc/systemd/system/bluetooth.service.d/debug.conf && systemctl daemon-reload && systemctl restart bluetooth'
journalctl -u bluetooth -f | grep -iE "reason|avctp|set_status"
Harmless noise you can ignore: AVRCP: failed to init uinput (the uinput
kernel module is absent on many vendor kernels; it only affects remote-control
key forwarding, not audio or AVRCP status).