지난 포스팅에서 게임패드의 GATT Service 구조를 파헤쳐 보았습니다.
BLE를 통해 HID 장치를 지원할 수 있도록 하는 방법인 HOGP(HID Over GATT Profile)에 대해 알게 되었고, HID Report Map Characteristic을 해석하여 게임패드 데이터를 담고 있는 Report ID의 Characteristic의 파싱 방법을 알아내었습니다.
•
Report ID 1: 게임패드 데이터 (19 bytes)
•
Report ID 3: 진동 (8 bytes)
이제 ESP32를 통해 게임패드와 연결하고 앞에서 찾은 Characteristic의 데이터를 읽어볼 차례입니다.
ESP32 펌웨어 구조 정리
ESP32의 개발용 프레임워크인 ESP-IDF는 FreeRTOS를 사용합니다. 코드의 정리를 위해 우선 펌웨어의 전체 구조를 짜보았습니다.
콜백들을 ble.c에 집어넣었더니 코드가 너무 지저분해져 extern으로 함수를 분리하였습니다.
NimBLE 스택 API를 사용하는 레이어의 함수들을 담고 있습니다. 예를 들어 task_sender.c에서 데이터를 전송하는 경우 ble.c의 함수를 호출합니다.
FreeRTOS의 Queue API를 사용하는 레이어입니다. ble.c처럼 묶어둔 함수들입니다.
BLE Task
BLE Task에서는 BLE와 관련된 역할을 수행합니다.
•
BLE 스택 실행
•
GAP/GATT 이벤트 처리 및 다른 Task로 이벤트 전송
•
다른 Task에서 이벤트 수신해 BLE 장치로 전송
ESP-IDF Bluetooth API Docs 중 일부 (바로가기)
ESP-IDF는 BLE 스택으로 Classic과 LE 모두 지원하는 Bluedroid와 LE만을 지원하는 NimBLE 두 가지 선택지를 제공합니다. 이번 프로젝트에서는 BLE만을 사용하기 때문에 공식 Docs의 권장사항대로 NimBLE을 사용하기로 하였습니다.
처음 사용하는 블루투스 스택이다 보니 예제를 많이 참고하였는데요, 상당히 특이한 함수를 사용하고 있었습니다.
예제 중 일부
이런 식으로 peer_ 으로 시작하는 함수들이 바로 그 녀석들입니다.
esp-idf/examples/bluetooth/nimble/common/nimble_central_utils/esp_central.h
함수가 정의된 경로로 찾아가 함수들을 살펴보니 Connection, Service, Characteristic의 NimBLE Handler를 관리해주는 역할을 하고 있었습니다.
상당히 편리해 보여 이번 프로젝트에 적극 활용하기로 하였습니다.
함수들은 ESP-IDF 5.1 기준 아래 위치에 있습니다.
esp-idf/examples/bluetooth/nimble/common/nimble_central_utils/peer.c
Plain Text
복사
peer 함수들을 사용하기 위해서는 CMakeLists.txt에 아래 구문을 추가하고, esp_central.h를 include하여야 합니다.
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/bluetooth/nimble/common/nimble_central_utils)
Plain Text
복사
main/app/task/ble/ble_task.c
main/app/task/ble/ble.c
BLE Task에서 ble_start() 함수를 호출하면 Bluetooth와 NimBLE이 init됩니다. 예제의 init 순서를 그대로 가져왔습니다.
main/app/task/ble/ble_gap_event.c
init이 완료되면 scan을 시작하고, 사전에 기록해둔 게임패드의 Discovery Packet을 RAW로 비교하여 게임패드에 해당하는 장치만 연결을 시도합니다.
main/app/task/ble/ble_gap_event.c
main/app/task/ble/task_sender.c
HID Report ID 1 Characteristic의 notify는 NimBLE에서 자동으로 Eanble되며, 이벤트가 발생하면 이에 해당하는 Queue로 데이터를 전송하는 함수를 호출합니다.
main/app/task/ble/ble_task.c
한편 Rumble은 ble_rumble_queue_hd 이름의 큐(핸들러)를 감시하며, 데이터가 입력되는 경우 ble.c의 Rumble 전송 함수(ble_send_rumble())를 호출해 동작합니다.
Test Task
BLE의 기능을 테스트하기 위해 테스트 목적의 Task를 만들었습니다. BLE Task에서 전송하는 이벤트를 수신하며 아래 동작을 합니다.
•
게임패드 연결될 때 연결 피드백으로 Rumble 요청 전송
•
게임패드 입력 신호를 터미널에 테스트 출력
Rumble 데이터나 게임패드 입력 데이터는 비트 단위로 파싱을 하는데요, 비트 연산자를 사용해도 되지만 조금 더 멋지고 아름답게(?) 처리하기 위해 struct의 bit field를 활용하였습니다.
main/app/task/test/test_task.c
main/app/task/test/test_task.c
문제는 후자의 게임패드 데이터의 RAW 데이터 길이가 19 byte인 반면 bit field의 단위가 2 byte라 1 byte의 낭비가 발생한다는 점입니다(...)
멋지니 그냥 이대로 두죠 
main/app/task/test/test_task.c
Test Task는 gattc_event를 수신하는 함수인 s_process_gattc_event()를 반복적으로 호출합니다.
main/app/task/test/test_task.c
s_process_gattc_event() 함수의 내용을 보면 ble_gattc_event_queue_hd 큐를 계속 기다리는데요, 이벤트가 들어오면 flag에 따라 함수를 실행합니다.
main/app/task/test/test_task.c
만약 연결 상태 변경 이벤트(BLE_GATTC_EVENT_CONNECTION)인 경우 연결되었을 때 Rumble을 발생시키도록 하였고,
main/app/task/test/test_task.c
게임패드 데이터 입력 이벤트(BLE_GATTC_EVENT_DATA_PAD)의 경우 터미널에 스틱과 몇몇 버튼을 출력하도록 하였습니다.
테스트
의도한 내용과 같이 잘 작동하는 것을 확인할 수 있습니다.
이제 다음 단계인 USB HID 구현으로 넘어가도록 하죠!