연동 (IPC)

타사 도구가 BlueCaster 코어와 통신하는 방법입니다. 스트림 상태를 표시하고, 프로필을 시작·중지하고, 볼륨을 설정하거나, BlueCaster의 Bluetooth 링크를 통해 헤드폰 자체의 서비스에 접근할 수 있습니다. 이런 일을 하는 도구는 함께 쓰는 도구에 있습니다.

프로토콜은 2.0.2 베타 동안 아직 바뀔 수 있습니다. 기준이 되는 것은 app/src/crates/ipc/src/protocol.rs와 이로부터 생성된 app/src/ui/src/ipc/bindings의 TypeScript 타입입니다. 이 페이지는 그 사용법을 설명하며, 모든 필드를 나열하지는 않습니다.

목차

  1. 연결
    1. 프레임
    2. 핸드셰이크
  2. 상태와 이벤트
  3. 요청
    1. 오류와 메시지
    2. 메서드
  4. RFCOMM 패스스루
  5. 호환성
  6. 예제

연결

코어(bluecaster-core.exe)는 BlueCaster가 실행되는 동안, 창이 있든 없든 실행됩니다. 코어는 자신을 실행한 사용자의 named pipe에서 대기합니다.

\\.\pipe\bluecaster-<user SID>
  • <user SID>는 로그온한 사용자의 SID 문자열(S-1-5-21-...)로, 예를 들어 whoami /user로 알 수 있습니다. 사용자마다 각자의 코어와 파이프가 있으며, 파이프의 DACL은 그 사용자만 허용하고 원격 클라이언트는 거부됩니다.
  • 파이프는 바이트 모드입니다. 여러 클라이언트가 동시에 연결할 수 있으며(BlueCaster 창도 그중 하나), 각 클라이언트는 같은 상태와 이벤트를 받습니다.
  • 2.0.x까지는 파이프 이름이 \\.\pipe\a2dpwb-<user SID>였습니다. BlueCaster 3.0.0은 두 이름을 모두 제공하며, 이전 이름은 3.0.0 이후 최소 6개월 동안 계속 제공합니다. 클라이언트는 먼저 bluecaster-<user SID>를 시도하고, 안 되면 이전 이름으로 돌아가야 합니다. 프로토콜 버전은 1 그대로입니다.
  • 파이프가 없으면 코어가 실행되지 않은 것입니다. 이를 확인할 때는 코어의 뮤텍스가 아니라 파이프를 사용하십시오.
  • BLUECASTER_INSTANCE=<name>으로 시작한 개발 빌드는 대신 \\.\pipe\bluecaster-<SID>-<name>에서 대기하므로, 설치된 버전과 나란히 실행할 수 있습니다.

프레임

양방향의 모든 메시지는 프레임 하나입니다. JSON의 길이를 u32 little-endian으로 쓰고, 그 뒤에 JSON(UTF-8)이 옵니다. 4 MiB를 넘는 프레임은 거부됩니다.

핸드셰이크

  1. 클라이언트는 연결 후 5초 안에 hello를 보냅니다.
    {"type": "hello", "protocol": 1, "client": "mytool/1.2.0"}
    

    client는 자유로운 이름과 버전이며, 코어의 로그에 나타납니다.

  2. 코어는 welcome으로 응답하거나, reject로 응답하고 파이프를 닫습니다(프로토콜 버전이 다르거나 hello가 없을 때).
    {"type": "welcome", "protocol": 1, "server": "bluecaster-core/2.0.2-beta.12", "features": ["rfcomm"]}
    {"type": "reject", "reason": "protocol 2 not supported (core speaks 1)"}
    
  3. 코어는 전체 상태를 담은 snapshot을 보내고, 그다음 event를 보냅니다.

프로토콜 버전은 정확히 일치해야 합니다. 버전 1을 깨뜨리지 않는 추가 사항은 대신 features로 알립니다(호환성 참조).

sequenceDiagram
    participant C as 클라이언트
    participant B as bluecaster-core
    C->>B: hello (protocol 1)
    alt 프로토콜 일치
        B-->>C: welcome (features)
        B-->>C: snapshot (전체 상태, rev)
        loop 연결된 동안 (순서 무관)
            B-->>C: event (rev + 1)
            C->>B: request (id)
            B-->>C: response (같은 id)
        end
        B-->>C: bye (코어 종료 시)
    else 프로토콜이 다르거나 5초 안에 hello가 없음
        B-->>C: reject, 파이프 닫기
    end

상태와 이벤트

{"type": "snapshot", "rev": 41, "state": {"adapter": {...}, "devices": [...], "stream": {...}, "profiles": [...], "active_profile": "WH-1000XM5", "settings": {...}, ...}}
{"type": "event", "rev": 42, "event": {"kind": "stream", "value": {"state": "streaming", "profile": "WH-1000XM5", "info": {"codec": "ldac", "bitrate_kbps": 990, ...}, "volume": 80, ...}}}

클라이언트는 자체 상태를 갖지 않습니다. 마지막 스냅샷을 보관하고 각 이벤트를 거기에 적용합니다. 각 상태 이벤트는 스냅샷의 한 부분 전체를 대체합니다.

이벤트 kind 대체하는 부분
adapter adapter
devices devices
scanning, le_scanning scanning, le_scanning
stream stream (상태, 메시지, 프로필, 코덱 정보, 볼륨, 배터리)
profiles profiles와 active_profile
settings settings
update update
capture capture
v1_running, verbose, crashes 같은 이름의 필드

stream.state는 idle, connecting, streaming, reconnecting, error 중 하나이며, stream.message가 그 이유를 메시지 키로 알려 줍니다.

리비전. 모든 상태 이벤트는 다음 리비전(rev + 1)을 담고 있습니다. 번호가 건너뛰면 이벤트가 유실된 것이므로 state.snapshot을 보내 새 스냅샷을 받으십시오. 읽는 속도가 너무 느린 클라이언트도 요청하지 않아도 코어로부터 새 snapshot을 받습니다. 상태가 아닌 이벤트(link, log, receiver, rfcomm_data, rfcomm_closed)는 현재 리비전을 담으며 리비전을 올리지 않습니다.

코어가 종료될 때는 이벤트 bye를 보내고 파이프를 닫습니다.

요청

{"type": "request", "id": 7, "method": "stream.start", "params": {"profile": "WH-1000XM5"}}
{"type": "response", "id": 7, "result": null, "error": null}
  • id는 클라이언트가 고르는 임의의 숫자이며, 응답에 그대로 담겨 돌아옵니다. 요청은 동시에 실행되므로 응답이 요청과 다른 순서로 올 수 있습니다.
  • 매개변수가 없는 메서드에서는 params를 생략합니다.
  • 상태를 바꾸는 요청은 처리가 끝나면 응답하며, 그 변경은 이벤트로도 도착합니다. 클라이언트는 응답이 아니라 이벤트를 바탕으로 상태를 표시해야 합니다.

오류와 메시지

{"type": "response", "id": 8, "result": null, "error": {"code": "rfcomm.not_connected", "message": {"key": "ipc.error.rfcomm_not_connected", "args": []}}}

code는 프로그램용입니다(adapter.driver_missing, app.streaming, …). message는 상태 안의 상태 텍스트와 마찬가지로 BlueCaster 언어 파일(app/lang)의 번역 키와 printf 형식의 인수입니다. 도구는 거기에서 키를 찾아보거나, 코드에 맞는 자체 텍스트를 표시할 수 있습니다.

메서드

도구용 메서드:

메서드 매개변수 결과
state.snapshot – Snapshot
stream.start profile null. 실행 중인 스트림이 있으면 먼저 중지
stream.stop – null
stream.set_volume volume (AVRCP 절대 볼륨 0-127) null
profile.set_active name null
device.scan seconds (3-600) Device[], devices 이벤트로도 전달
device.scan_stop – null. 실행 중인 device.scan이 응답함
audio.devices – AudioDevice[] (캡처 소스)
diag.subscribe logs (수준 또는 null), link (bool), receiver (bool, 선택) null. 이 연결에 대해 log / link / receiver 이벤트를 시작하거나 중지
diag.info {"mask": bool} (선택) DiagInfo (“진단 정보 복사”의 텍스트)
rfcomm.open, rfcomm.send, rfcomm.close RFCOMM 패스스루 참조  

BlueCaster 창이 사용하며 도구는 주의해서만 써야 하는 메서드: profile.save / profile.delete, settings.set(설정을 모두 대체합니다. 일부만 담은 객체는 절대 보내지 말고, 최신 settings에 변경 사항을 더해 보내십시오), device.forget / device.rename, adapter.*(adapter.assign_winusb와 adapter.restore_driver는 UAC 프롬프트를 표시하고, 개발자 모드의 칩 도구인 adapter.probe와 adapter.run_script는 Bluetooth 스택을 중지합니다), app.quit. 그 밖의 메서드(stream.start_custom, 나머지 device.*(device.import_windows_pairings는 UAC 프롬프트를 표시), audio.apps, audio.mixer / audio.mixer_set, 위 두 가지 외의 diag.*, receiver.*, app.*)는 창 자체의 화면을 위한 것이며 예고 없이 바뀔 수 있습니다. 매개변수와 결과를 포함한 전체 목록은 protocol.rs의 Method enum입니다.

RFCOMM 패스스루

welcome.features에 "rfcomm"이 있을 때 사용할 수 있습니다(2.0.2-beta.10 이상). BlueCaster를 통해 연결된 헤드폰은 Windows에 보이지 않으므로, 도구가 BlueCaster가 이미 가진 링크를 통해 헤드폰 자체의 RFCOMM 서비스(제조사 제어 앱: 배터리, 노이즈 컨트롤, EQ)에 접근할 수 있게 합니다.

  • rfcomm.open {address, uuid} → {channel_id, mtu}. address는 스트리밍 중인 장치여야 하며, 코어는 이를 위해 연결하거나 페어링하지 않습니다(rfcomm.not_connected). uuid는 128비트 전체 UUID이거나, Bluetooth 기본 UUID 위의 16비트 "xxxx"입니다. 다른 open 뒤에 대기 중일 때도 응답은 20초 안에 오므로, 최소 25초는 기다리십시오. 오류: rfcomm.not_connected, rfcomm.service_not_found, rfcomm.open_failed, rfcomm.busy.
  • rfcomm.send {channel_id, data} → null. data는 base64(표준, 패딩 있음)이며 요청당 최대 64 KiB이고, 요청 순서대로 최대 mtu 크기의 프레임으로 전송됩니다. 오류: rfcomm.no_channel, rfcomm.busy(대기열이 가득 차서 전송되지 않음), rfcomm.timeout(전송되었는지 알 수 없음: 반복하지 말고 채널을 다시 여십시오).
  • 수신한 데이터는 채널을 연 연결에만 rfcomm_data 이벤트 {channel_id, data}로 도착합니다.
  • rfcomm.close {channel_id} → null. 그 전에 보낸 데이터가 전송된 뒤에 닫히며, 그 뒤에 rfcomm_closed는 오지 않습니다.
  • 채널은 파이프 연결이 끝날 때도 닫히고, rfcomm_closed 이벤트 {channel_id, reason}과 함께 닫히기도 합니다: remote(장치가 닫았거나 링크가 끊김), disconnected(A2DP 연결이 끝남), stopped(Bluetooth 스택이 중지됨), overflow(클라이언트가 읽지 않은 수신 데이터가 1 MiB를 넘음).

호환성

  • 기능. 추가 사항을 사용하기 전에 welcome.features를 확인하십시오. 2.0.2-beta.10 이전의 코어에는 이 필드가 없습니다.
  • 알 수 없는 요청. 코어가 읽을 수 없는 요청(알 수 없는 메서드, 잘못된 매개변수)에는 오류 ipc.bad_request로 응답하며, 연결은 유지됩니다. 2.0.2-beta.10 이전의 코어는 대신 연결을 닫습니다. 그 밖의 읽을 수 없는 프레임은 연결을 닫습니다.
  • 새 필드. 코어는 시간이 지나면서 상태와 이벤트에 필드를 추가합니다. 모르는 필드는 무시하고, 그 때문에 메시지를 거부하지 마십시오.
  • 3.0.0에서의 이름 변경(#193): 파이프는 \\.\pipe\bluecaster-<SID>가 됩니다. 코어는 3.0.0 이후 최소 6개월 동안 같은 프로토콜로 \\.\pipe\a2dpwb-<SID>에서도 계속 대기하며, 이를 없앨 때는 먼저 릴리스 노트에서 알립니다. 먼저 bluecaster-<SID>를 시도하고, 안 되면 a2dpwb-<SID>로 돌아가십시오. 코어의 뮤텍스는 Local\BlueCaster_Core가 되며, 같은 기간 동안 Local\A2DPWB_v2_Core도 계속 유지하고, 두 이름의 제거는 함께 공지합니다.

예제

스트림 상태를 출력하고 그 변화를 따라가는 최소한의 Python 클라이언트(표준 라이브러리만 사용):

import json, struct, subprocess

sid = subprocess.run(["whoami", "/user", "/fo", "csv", "/nh"],
                     capture_output=True, text=True).stdout.strip().split(",")[1].strip('"')
pipe = open(rf"\\.\pipe\bluecaster-{sid}", "r+b", buffering=0)

def send(msg):
    b = json.dumps(msg).encode()
    pipe.write(struct.pack("<I", len(b)) + b)

def read_exact(n):
    buf = b""
    while len(buf) < n:
        chunk = pipe.read(n - len(buf))
        if not chunk:
            raise EOFError
        buf += chunk
    return buf

def recv():
    (n,) = struct.unpack("<I", read_exact(4))
    return json.loads(read_exact(n))

send({"type": "hello", "protocol": 1, "client": "example/0.1"})
welcome = recv()
assert welcome["type"] == "welcome", welcome
snap = recv()
print("stream:", snap["state"]["stream"]["state"])
while True:
    m = recv()
    if m["type"] == "event" and m["event"]["kind"] == "stream":
        s = m["event"]["value"]
        print("stream:", s["state"], (s.get("info") or {}).get("codec"))
    elif m["type"] == "event" and m["event"]["kind"] == "bye":
        break

설치된 BlueCaster를 건드리지 않고 도구를 시험하려면, BLUECASTER_INSTANCE와 BLUECASTER_CONFIG_DIR을 설정한 개발 빌드를 실행하고(빌드 참조) 그 파이프에 연결하십시오.