Выбирая геймпад для своего компьютера, я остановился на DualShock4, так как мне понравилась идея, что можно будет слушать аудио через подключаемые к нему наушники. Но после покупки я узнал, что, оказывается, никто не знает, как передать звук на геймпад через Bluetooth. Поэтому я решил разобраться с данным вопросом. Если вам интересно узнать, как DualShock4 общается с игровой консолью, жду под катом.
К сожалению, у меня нет PlayStation 4, поэтому пришлось довольствоваться только выложенными в Интернете дампами, а также уже известными фрагментами обмена.
В процессе изучения темы мне очень помогла вот эта страница. В ней описаны основные моменты передачи данных между консолью и геймпадом, а также выложен дамп этих данных. Нас интересует файл дампа с именем ds4_uart_hci_cap_playroom_needs_sorting.pcap.gz. Открываем его в Wireshark и начинаем изучать. Отсортируем пакеты по времени, так как, видимо, дамп записывался отдельно на приём и передачу. Дамп снимался напрямую с UART геймпада, после чего был сконвертирован в pcap.
В начале идёт настройка самого модуля Bluetooth. Далее, с №49-го по №163-й пакет, идёт установка соединения и настройка канала передачи. Очень хорошо этот процесс описан в статье Беспроводной звук. Часть 1. Препарируем Bluetooth.
Но для нашей задачи это неособо важно.
После всех «подготовительных работ» геймпад начинает отправлять HID Report. Формат сообщения описан на вики странице. Первый пакет с данными от консоли — это пакет №70181. Давайте разберём его, пользуясь данными с вики страницы.
Нас интересуют только данные, которые передаются через HID Profile.
Вот его содержание.
Номер байта | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|---|
[0] | 0x0a – Тип Data | 0x00 — Зарезервировано | 0x02 — Направление передачи | |||||
[1] | 0x11 – Код операции | |||||||
[2 — 3] | Неизвестно | |||||||
[4] | 0xf0 Запрещает изменение данных у геймпада, 0xf3 Разрешает изменение | |||||||
[5 — 6] | Неизвестно | |||||||
[7] | Rumble (right / weak) | |||||||
[8] | Rumble (left / strong) | |||||||
[9] | RGB color (Red) | |||||||
[10] | RGB color (Green) | |||||||
[11] | RGB color (Blue) | |||||||
[12-24] | Неизвестно | |||||||
[25] | Громкость звучания в % | |||||||
[26 — 74] | Неизвестно | |||||||
[75 — 78] | CRC-32 от предыдущих данных |
#!/usr/bin/env python3
from pcapfile import savefile
import collections
import struct
class bluetooth(object):
def __init__(self, packet, number):
self.direction = packet.raw()[3]
self.payload = packet.raw()[4:]
self.time = ((packet.timestamp_ms-444738)/1000000)+(packet.timestamp-3)
self.number = number
pcap = savefile.load_savefile(open('ds4_uart_hci_cap_playroom_needs_sorting.pcap', 'rb'))
bluetooth_packet = []
number=1
for pkt in pcap.packets:
bluetooth_packet.append(bluetooth(pkt, number))
number+=1
sbc = open('test.sbc', 'wb')
bluetooth_packet.sort(key=lambda pkt: pkt.time)
count = 0
for bt in bluetooth_packet:
count+=1
if(bt.payload[0]==2):
l2cap_len = struct.unpack("<H",bt.payload[5:7])[0]
if(l2cap_len>5):
sony_opcode = bt.payload[10]
if(sony_opcode == 0x19):
sbc.write(bt.payload[0x5b:-0x12])
if(sony_opcode == 0x17):
sbc.write(bt.payload[0x10:-0x8])
if(sony_opcode == 0x15):
sbc.write(bt.payload[0x5b:-0x1D])
if(sony_opcode == 0x14):
sbc.write(bt.payload[0x10:-0x28])
gst-launch-1.0 filesrc location=test.sbc ! sbcparse ! sbcdec ! autoaudiosink
gst-launch-1.0 filesrc location=test.sbc ! sbcparse ! sbcdec ! audioconvert ! wavenc ! filesink location=output.wav
#!/usr/bin/env python3
import struct
from sys import stdin
import os
from io import FileIO
hiddev = os.open("/dev/hidraw5", os.O_RDWR | os.O_NONBLOCK)
pf = FileIO(hiddev, "wb+", closefd=False)
#pf=open("ds_my.bin", "wb+")
rumble_l = 0
rumble_r = 0
r = 0
g = 0
b = 50
crc = 0
volume = 50
flash_bright = 150
flash_dark = 150
def frame_number(inc):
res = struct.pack("<H", frame_number.n)
frame_number.n += inc
if frame_number.n > 0xffff:
frame_number.n = 0
return res
frame_number.n = 0
def joy_data():
data = [0xf3,0x4,0x00]
data.extend([rumble_l,rumble_r,r,g,b,flash_bright,flash_dark])
data.extend([0]*8)
data.extend([0x43,0x43,0x00,volume,0x85])
return data
def _11_report():
data = joy_data()
data.extend([0]*(48))
data.append(crc)
return bytearray(data)
def _14_report(audo_data):
return b'\x14\x40\xA0'+ frame_number(2) + b'\x02'+ audo_data + bytearray(40)
def _15_report(audo_data):
data = joy_data();
data.extend([0]*(52))
return b'\x15\xC0\xA0' + bytearray(data)+ frame_number(2) + b'\x02' + audo_data + bytearray(29)
def _17_report(audo_data):
return b'\x17\x40\xA0' + frame_number(4) + b'\x02' + audo_data + bytearray(8)
stdin = stdin.detach()
data = bytearray()
count = 1
while True:
# if count % 200:
if True:
data = _14_report(stdin.read(224)) if count % 3 else _15_report(stdin.read(224))
else:
data = _17_report(stdin.read(448))
print('big')
count+=1
pf.write(data)
gst-launch-1.0 -q audiotestsrc is-live=true ! sbcenc ! 'audio/x-sbc,channels=2,rate=32000,channel-mode=dual,blocks=16,subbands=8,bitpool=25' ! queue ! fdsink | ./play.py
gst-launch-1.0 -q pulsesrc device="alsa_output.pci-0000_00_1b.0.analog-stereo.monitor" ! queue ! audioresample ! 'audio/x-raw,rate=32000' ! audioconvert ! sbcenc ! 'audio/x-sbc,channels=2,rate=32000,channel-mode=dual,blocks=16,subbands=8,bitpool=25' ! queue ! fdsink | ./play.py
pacmd list-sources | grep -e device.string -e 'name:'
К сожалению, не доступен сервер mySQL