Android로 Bluetooth HID 장치(Joystick) 만들기 - 01

개요

Android 9(API Level 28, Pie)에서는 휴대 전화를 Bluetooth HID로 사용할 수 있도록 지원하므로 키보드, 마우스 또는 게임 패드로 사용할 수 있습니다.

BluetoothHidDevice를 사용해서 Bluetooth Joystick을 구현하는 것을 설명하겠습니다.

해당 소스는 https://github.com/Jung-Max/BleHidJoystick를 참고하시면 됩니다.

앱 다운로드는 https://play.google.com/store/apps/details?id=com.ckbs.blehidjoystick를 참고하세요.

Bluetooth 준비하기

가장 먼저 Bluetooth를 설정하고 적절한 프로파일을 가져와야 합니다.

처음으로 블루투스 어댑터를 가져옵니다.

아직 활성화 되지 않은 경우 블루투스를 활성화 해야합니다.

1
2
3
4
5
6
7
8
9
private BluetoothAdapter mBtAdapter = BluetoothAdapter.getDefaultAdapter();

// Get bluetooth enabled before continuing
if (!mBtAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
} else {
btListDevices();
}

그런 다음 기존 페어링된 장치의 목록을 가져와야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private ArrayList<BluetoothDevice> mDevices = new ArrayList<>();

private void btListDevices() {

Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices();

// Add devices to adapter
List<String> names = new ArrayList<>();

// add empty
names.add("(disconnected)");
mDevices.add(null);

for (BluetoothDevice btDev : pairedDevices) {
names.add(btDev.getName());
mDevices.add(btDev);
}

...

}

BluetoothHidDevice 취득하기

블루투스 기기의 정보를 취득 후에는 활성화한 HID 서비스와 통신할 수 있도록 프록시(BluetoothHidDevice)를 가져와야 합니다.

이것은 BluetoothAdapter.getProfileProxy()을 사용해서 취득 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mBtAdapter.getProfileProxy(this, new BluetoothProfile.ServiceListener() {
@Override
@SuppressLint("NewApi")
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (profile == BluetoothProfile.HID_DEVICE) {
Log.d(TAG, "Got HID device");
mBtHidDevice = (BluetoothHidDevice) proxy;

...

mBtHidDevice.registerApp();
}
}

@Override
public void onServiceDisconnected(int profile) {
if (profile == BluetoothProfile.HID_DEVICE) {
Log.d(TAG, "Lost HID device");
}
}
}, BluetoothProfile.HID_DEVICE);

이 호출은 기본적으로 프록시를 요청하고 저장합니다.

registerApp()

Bluetooth HID의 registerApp()의 파라미터는 아래와 같습니다.

1
2
3
4
5
6
public boolean registerApp(
BluetoothHidDeviceAppSdpSettings sdp,
BluetoothHidDeviceAppQosSettings inQos,
BluetoothHidDeviceAppQosSettings outQos,
Executor executor,
Callback callback)

sdp

BluetoothHidDeviceAppSdpSettings는 Bluetooth HID 장치 응용 프로그램의 SDP (Service Discovery Protocol) 설정을 나타냅니다.

BluetoothHidDevice 프레임 워크는 앱 등록 중에 SDP 레코드를 추가하여 Android 장치를 Bluetooth HID 장치로 검색 할 수 있습니다.

BluetoothHidDeviceAppSdpSettings의 파라미터를 살펴보면 다음과 같습니다.

1
2
3
4
5
public BluetoothHidDeviceAppSdpSettings (String name, 
String description,
String provider,
byte subclass,
byte[] descriptors)

name, description, provider은 각자 목적에 맞는 문자열을 넣어주면 됩니다.(50byte 이하로만)

subclass는 HID 스팩문서의 4.2 Subclass를 참고합니다.

Subclass

subclass는 0 또는 1만 설정할 수 있습니다. 보통은 0으로 설정하게 됩니다.

HID descriptors

descriptors는 HID 스팩문서의 6절을 보면 나와있습니다.

디스크립터는 기본 HID 디스크립터로 구성되며 그 뒤에 다른 디스크립터가 있습니다 (문자 적으로 연결됨).

아래 테이블은 주 디스크립터의 레이아웃입니다. (추가 디스크립터는 하나만 필요하며 추가 디스크립터의 필드는 선택 사항 임).

Part Byte(s) Description
bLength 0 Length of main descriptor (not including other descriptor) - 0x09
bDescriptorType 1 HID descriptor type - 0x21
bcdHID 2-3 HID specification version, in little endian binary coded decimal - 0x11 0x01
bCountryCode 4 Country code - 0x00 for not localised (only relevant for keyboards)
bNumDescriptors 5 Number of extra descriptors - 0x01 (the report descriptor)
bDescriptorType 6 Report type 0x22
wDescriptorLength 7-8 Length of the report descriptor type, in little endian

HID 디스크립터와 관련된 HID descriptor tooltutorial about USB HID report descriptors를 참고하면 더 많은 것을 알 수 있습니다.

최종적으로 4 buttons, 1 X/Y joystick의 descriptor은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private static final byte[] descriptor = new byte[] {
// HID descriptor
0x09, // bLength
0x21, // bDescriptorType
0x11, 0x01, // bcdHID
0x00, // bCountryCode
0x01, // bNumDescriptors
0x22, // bDescriptorType
0x30, 0x00, // wDescriptorLength (48 in decimal)

// Report descriptor - 4 buttons, 1 X/Y joystick
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x05, // USAGE (Game Pad)
(byte) 0xa1, 0x01, // COLLECTION (Application)
(byte) 0xa1, 0x00, // COLLECTION (Physical)
0x05, 0x09, // USAGE_PAGE (Button)
0x19, 0x01, // USAGE_MINIMUM (Button 1)
0x29, 0x04, // USAGE_MAXIMUM (Button 4)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
(byte) 0x95, 0x04, // REPORT_COUNT (4)
(byte) 0x81, 0x02, // INPUT (Data,Var,Abs)
0x75, 0x04, // REPORT_SIZE (4)
(byte) 0x95, 0x01, // REPORT_COUNT (1)
(byte) 0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x15, (byte) 0x81, // LOGICAL_MINIMUM (-127)
0x25, 0x7f, // LOGICAL_MAXIMUM (127)
0x75, 0x08, // REPORT_SIZE (8)
(byte) 0x95, 0x02, // REPORT_COUNT (2)
(byte) 0x81, 0x02, // INPUT (Data,Var,Abs)
(byte) 0xc0, // END_COLLECTION
(byte) 0xc0 // END_COLLECTION
};

inQos, outQos

수신 / 발신시의 QoS 셋팅과 관련된 파라미터 입니다. 기본적으로 null을 사용합니다.

BluetoothHidDeviceAppQosSettings는 Bluetooth HID 장치 응용 프로그램의 QoS(서비스 품질) 설정을 나타냅니다.

BluetoothHidDevice 프레임 워크는 등록 중에 앱의 L2CAP QoS 설정을 업데이트합니다.

Parameters 설명
serviceType L2CAP service type, default = SERVICE_BEST_EFFORT
tokenRate L2CAP token rate, default = 0
tokenBucketSize L2CAP token bucket size, default = 0
peakBandwidth L2CAP peak bandwidth, default = 0
latency L2CAP latency, default = MAX
delayVariation L2CAP delay variation, default = MAX

executor

Executor는 콜백이 실행될 Executor 객체입니다. Executor 객체가 필요합니다.

Executors.newSingleThreadExecutor()로 Executor 객체를 생성해 줍니다.

callback

callback은 콜백 메시지가 전송 될 콜백 객체입니다. 콜백 객체가 필요합니다.

기본적으로 callback에는 특별한 설정을 하지 않아도 됩니다.

하지만 연결 준비가 완료된 시점과 종료 된 시점을 알 수 있도록 onConnectionStateChanged를 구현하면 도움이됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
new BluetoothHidDevice.Callback() {
@Override
public void onGetReport(BluetoothDevice device, byte type, byte id, int bufferSize) {
Log.v(TAG, "onGetReport: device=" + device + " type=" + type
+ " id=" + id + " bufferSize=" + bufferSize);
}

@Override
public void onConnectionStateChanged(BluetoothDevice device, final int state) {
Log.v(TAG, "onConnectionStateChanged: device=" + device + " state=" + state);
if (device.equals(mBtDevice)) {
runOnUiThread(new Runnable() {
@Override
public void run() {
TextView status = findViewById(R.id.status);
if (state == BluetoothProfile.STATE_DISCONNECTED) {
status.setText(R.string.status_disconnected);
mBtDevice = null;
} else if (state == BluetoothProfile.STATE_CONNECTING) {
status.setText(R.string.status_connecting);
} else if (state == BluetoothProfile.STATE_CONNECTED) {
status.setText(R.string.status_connected);
} else if (state == BluetoothProfile.STATE_DISCONNECTING) {
status.setText(R.string.status_disconnecting);
}
}
});
}
}
}

연결

최종적으로 앱을 등록하고 장치를 연결할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BluetoothHidDeviceAppSdpSettings sdp = new BluetoothHidDeviceAppSdpSettings(
"BleHidJoystick",
"Android BLE HID Joystick",
"Android",
(byte) 0x00,
descriptor
);

mBtHidDevice.registerApp(sdp, null, mBluetoothHidDeviceAppQosSettings,
Executors.newSingleThreadExecutor(), new BluetoothHidDevice.Callback() {
... //omitted
});

mBtHidDevice.connect(device);
}

다음 포스트에서는 연결된 장비로 어떻게 input을 보내는지 설명하겠습니다.

공유하기