Fix Bluetooth bonding and record verified headphone relay fix
One-shot bluetoothctl pairing exited before SSP finished, leaving the headphone paired but not bonded, so it was lost on reboot. Pair inside a single bluetoothctl session with a persistent agent, and clear temporary pairings first. Document that SBC medium + 500 ms buffer removed the iPhone -> Radxa -> headphone dropouts, verified by listening and logs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ Mac/iPhone의 AirPlay 2 ── Shairport Sync ─┐
|
||||
- Bluetooth: Debian의 BlueALSA 패키지. 기준 보드 버전 **4.3.1**.
|
||||
- 수신과 송신은 A2DP 음악 프로필을 사용합니다. 기준 보드에서는 SBC로 연결됐습니다.
|
||||
- SBC 송신 품질은 medium입니다. 기준 보드에서 high로 동시 수신·송신 시 끊겨
|
||||
전송량을 줄였습니다. 송신 버퍼는 500 ms이며 안정성은 청취 검증이 필요합니다.
|
||||
전송량을 줄였습니다. 송신 버퍼는 500 ms이며, 이 조합에서 끊김 없는 청취를 확인했습니다.
|
||||
- DAC 출력은 44.1 kHz / 스테레오 / 16비트, 블루투스용 가상 믹서는 48 kHz입니다.
|
||||
- 각 입력을 3 dB 낮춥니다. 여러 입력의 큰 소리가 겹쳐 찢어지면 입력 볼륨을 낮추세요. 지연 없는 실시간 녹음/악기 모니터링용 구성은 아닙니다.
|
||||
- 각 송신 기기의 볼륨으로 믹스 비율을 바꿉니다.
|
||||
@@ -157,6 +157,7 @@ sudo radxa-audio pair AA:BB:CC:DD:EE:FF --output
|
||||
재부팅 후 등록을 유지하려면 장비를 페어링 모드로 켠 뒤
|
||||
`sudo radxa-audio pair 주소 --output --bond`를 사용하세요. 웹의 새 장비 등록도
|
||||
영구 등록 성공을 확인하며, 인증 실패는 성공으로 표시하지 않습니다.
|
||||
임시 등록만 남은 장비는 기록을 지운 뒤 하나의 bluetoothctl 세션에서 다시 페어링합니다.
|
||||
이미 영구 등록된 장비는 다시 페어링하지 않습니다.
|
||||
|
||||
이미 등록한 장비로 전환:
|
||||
|
||||
+7
-2
@@ -109,8 +109,13 @@ sudo journalctl -u radxa-mixer-web -n 40 --no-pager
|
||||
에서 `Missing RTP packets`는 입력 수신 패킷 손실, `underrun`/`overrun`은
|
||||
오디오 버퍼 오류를 뜻합니다. 수신 손실은 버퍼만 늘려 복구되지 않습니다.
|
||||
같은 입력을 DAC로 보낼 때와 Bluetooth 출력으로 보낼 때를 비교하세요.
|
||||
현재 송신 버퍼는 500 ms이며 지연이 추가됩니다. 같은 컨트롤러의 동시
|
||||
수신·송신 안정성은 실제 청취 검증이 필요합니다.
|
||||
기준 보드에서는 버퍼만 500 ms로 늘려서는 끊김이 남았고, 송신 SBC 품질을
|
||||
high에서 medium으로 낮춘 뒤(`bluealsa --sbc-quality=medium`) 해결됐습니다.
|
||||
같은 컨트롤러로 수신과 송신을 동시에 하므로 송신 전송량이 수신 손실에 영향을 줍니다.
|
||||
현재 송신 버퍼는 500 ms이며 그만큼 지연이 추가됩니다.
|
||||
|
||||
청취 확인 시에는 `bluealsa-aplay -l`의 PLAYBACK 목록에 헤드폰이 있는지 먼저 봅니다.
|
||||
없으면 헤드폰이 휴대폰에 직접 연결돼 Radxa를 거치지 않은 소리일 수 있습니다.
|
||||
|
||||
## Bluetooth 컨트롤러 점검
|
||||
|
||||
|
||||
+16
-4
@@ -41,15 +41,27 @@ medium과 500 ms 버퍼를 저장했고 출력 등록 복구 후 다시 검증
|
||||
확인했습니다. Bluetooth 서비스 재시작만으로 복구되지 않아 보드를 재부팅했습니다.
|
||||
SBC 품질 변경으로 컨트롤러 오류가 해결됐다고 보지는 않습니다.
|
||||
|
||||
보스는 현재 `Paired: yes / Bonded: no`인 임시 연결입니다. 영구 등록 시도가
|
||||
인증 실패하여 재부팅 후 보스 등록 유지가 검증됐다고 주장하지 않습니다.
|
||||
재부팅 후 Wi-Fi/USB 관리 연결이 돌아오지 않아 전원을 재연결했고,
|
||||
컨트롤러는 정상 초기화됐습니다. 500 ms 버퍼와 SBC medium 설정은 유지됐습니다.
|
||||
임시 등록이던 보스는 재부팅으로 등록이 사라졌습니다.
|
||||
|
||||
일회성 `bluetoothctl --agent NoInputNoOutput pair`는 인증 완료 전에 종료돼
|
||||
`Bonded: no`로 남았던 것으로 보고, 하나의 bluetoothctl 세션에서
|
||||
에이전트 등록 → 검색 → 페어링을 진행하도록 바꿨습니다. 이 방식으로 보스가
|
||||
`Paired/Bonded/Trusted: yes`로 영구 등록됐습니다.
|
||||
|
||||
2026-10-12 19:00 전후로 iPhone → Radxa Bluetooth → 보스 경로(PCM 연결을
|
||||
`bluealsa-aplay -l`로 확인)에서 사용자가 정상 청취를 확인했습니다. 해당 구간
|
||||
로그에서 `Missing RTP packets`, underrun/overrun, HCI 시간 초과는 없었습니다.
|
||||
따라서 SBC medium + 500 ms 버퍼 조합으로 끊김이 해소된 것으로 판단합니다.
|
||||
장시간 재생 안정성은 아직 확인하지 않았습니다.
|
||||
이미 영구 등록된 휴대폰 등록은 재부팅 후 유지됐습니다.
|
||||
|
||||
## 실제 청취로 확인한 범위
|
||||
|
||||
사용자가 AirPlay 재생, AirPlay + 아이폰 Bluetooth 입력의 DAC 믹스 재생을
|
||||
정상 청취했다고 확인했습니다. 헤드폰의 실제 믹스 청취 확인은 아직 받지
|
||||
못했으며, 송신 프로필 연결 및 가상 믹서의 데이터 전달까지 검증했습니다.
|
||||
정상 청취했다고 확인했습니다. iPhone Bluetooth 입력 → 보스 헤드폰 출력도
|
||||
SBC medium / 500 ms 버퍼 설정에서 끊김 없이 청취했다고 확인했습니다.
|
||||
|
||||
## 아직 검증하지 않은 범위
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
- 출력 장비 주소: `E4:58:BC:C3:3B:F8` (기존 장비 참고만; 새 설치 기본값이 아님).
|
||||
- 웹 믹서: `http://192.168.10.83:8088`, `radxa-mixer-web.service` 자동 시작.
|
||||
- 공통 감쇠 −3 dB. AirPlay는 기존 네이티브 볼륨만 제어.
|
||||
- 보스는 현재 임시 등록(`Bonded: no`)이며 영구 등록은 추가 확인 필요.
|
||||
- 보스는 영구 등록(`Bonded: yes`, Trusted) 상태.
|
||||
- Wi-Fi 드라이버: aic8800_fdrv; `ps_on=0 he_on=0` 설정을 재부팅 후 확인함.
|
||||
- USB 관리 주소 10.55.0.1/24는 별도 gadget/systemd-networkd 설정이며,
|
||||
이 오디오 설치 자료가 생성하거나 변경하지 않음.
|
||||
@@ -18,8 +18,8 @@
|
||||
사용자가 정상 청취를 확인했습니다. Bose 헤드폰은 페어링/신뢰 등록 및
|
||||
A2DP SBC 48 kHz 출력 연결과 가상 믹서 전송까지 확인했습니다.
|
||||
사용자는 이후 iPhone → 보스 경로의 소리 끊김을 보고했습니다. 수신 패킷 손실과
|
||||
버퍼 오류를 확인했습니다. 500 ms 버퍼와 SBC medium 송신 설정을 적용했으나,
|
||||
보스 출력 연결이 실패하여 경로 복구와 청취 검증이 필요합니다.
|
||||
버퍼 오류를 확인했습니다. 500 ms 버퍼와 SBC medium 송신 설정을 적용하고
|
||||
보스를 영구 등록한 뒤, iPhone → Radxa → 보스 경로의 정상 청취를 확인했습니다.
|
||||
|
||||
기존 설정 백업은 보드의 `/root/audio-mixer-backup-20261012-162401/`와
|
||||
`/root/radxa-airplay2-backup-20261011-193842/`에 있습니다.
|
||||
|
||||
+27
-3
@@ -269,12 +269,13 @@ def pair(addr, output=False, bond=False):
|
||||
raise ValueError('장비 주소 형식이 잘못됐습니다.')
|
||||
info = run('bluetoothctl', 'info', addr, check=False, capture=True).stdout
|
||||
if 'Paired: yes' not in info or (bond and 'Bonded: yes' not in info):
|
||||
# Outgoing pairing agent lives only for this operation.
|
||||
controller = run('bluetoothctl', 'show', capture=True).stdout
|
||||
was_pairable = 'Pairable: yes' in controller
|
||||
run('bluetoothctl', 'pairable', 'on')
|
||||
if 'Paired: yes' in info:
|
||||
# A temporary (unbonded) pairing blocks a new link key; start clean.
|
||||
run('bluetoothctl', 'remove', addr, check=False, capture=True)
|
||||
try:
|
||||
run('bluetoothctl', '--agent', 'NoInputNoOutput', '--timeout', '25', 'pair', addr, check=False)
|
||||
agent_pair(addr)
|
||||
finally:
|
||||
if not was_pairable:
|
||||
run('bluetoothctl', 'pairable', 'off', check=False)
|
||||
@@ -294,6 +295,29 @@ def pair(addr, output=False, bond=False):
|
||||
print('페어링/신뢰 등록 완료:', addr)
|
||||
|
||||
|
||||
def agent_pair(addr, scan_seconds=12, pair_seconds=20):
|
||||
"""Pair inside one bluetoothctl session so the agent stays registered.
|
||||
|
||||
A one-shot `bluetoothctl --agent ... pair` can exit before Secure Simple
|
||||
Pairing finishes, which leaves the device paired but not bonded.
|
||||
"""
|
||||
import time
|
||||
proc = subprocess.Popen(['bluetoothctl'], stdin=subprocess.PIPE, text=True)
|
||||
steps = [('agent NoInputNoOutput', 1), ('default-agent', 1), ('pairable on', 1),
|
||||
('scan on', scan_seconds), ('scan off', 1), ('pair ' + addr, pair_seconds)]
|
||||
try:
|
||||
for command, wait in steps:
|
||||
proc.stdin.write(command + '\n')
|
||||
proc.stdin.flush()
|
||||
time.sleep(wait)
|
||||
proc.stdin.write('quit\n')
|
||||
proc.stdin.flush()
|
||||
proc.wait(timeout=10)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description='Radxa AirPlay + Bluetooth 믹서 관리')
|
||||
sub = p.add_subparsers(dest='cmd', required=True)
|
||||
|
||||
@@ -69,8 +69,11 @@ class ManagerTests(unittest.TestCase):
|
||||
def test_bond_request_must_not_report_temporary_pair_as_success(self):
|
||||
def fake(*args, **kwargs):
|
||||
return subprocess.CompletedProcess(args,0,stdout='Paired: yes\nBonded: no')
|
||||
with patch.object(m,'root'),patch.object(m,'run',side_effect=fake) as run:
|
||||
with patch.object(m,'root'),patch.object(m,'run',side_effect=fake) as run, \
|
||||
patch.object(m,'agent_pair') as agent:
|
||||
with self.assertRaises(ValueError):m.pair('AA:BB:CC:DD:EE:FF',output=True,bond=True)
|
||||
agent.assert_called_once_with('AA:BB:CC:DD:EE:FF')
|
||||
self.assertTrue(any('remove' in call.args for call in run.call_args_list))
|
||||
self.assertFalse(any('trust' in call.args for call in run.call_args_list))
|
||||
|
||||
def test_bond_request_keeps_existing_bond(self):
|
||||
|
||||
Reference in New Issue
Block a user