Category (Click)
개발보드 덕질하기

[GamePad Bridge] ESP32에서 BLE를 통해 게임패드 연결, 상호작용

 차완기 - @11/20/2023, 11:33:00 AM
GamePad Bridge Project : 에오르제아 행복 패드 힐러 라이프를 위하여
Xbox Elite Pad 후면 패들의 하드웨어 매퍼를 만드는 프로젝트입니다.
1.
프로젝트 시작
2.
BLE 및 HID 프로토콜의 이해
지난 포스팅에서 게임패드의 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를 사용합니다. 코드의 정리를 위해 우선 펌웨어의 전체 구조를 짜보았습니다.
 main
 app
 queue
 shair - Queue 핸들러, 데이터 타입 정의
 queue.c - Queue init
 queue.h
 task
 ble - NimBLE init, GAP/GATT 이벤트 처리
 ble_gap_event.c - GAP/GATT 이벤트 콜백 분리
콜백들을 ble.c에 집어넣었더니 코드가 너무 지저분해져 extern으로 함수를 분리하였습니다.
 ble.c - BLE 인터페이스 함수
NimBLE 스택 API를 사용하는 레이어의 함수들을 담고 있습니다. 예를 들어 task_sender.c에서 데이터를 전송하는 경우 ble.c의 함수를 호출합니다.
 ble.h
 task_sender.c - RTOS Queue 인터페이스 함수
FreeRTOS의 Queue API를 사용하는 레이어입니다. ble.c처럼 묶어둔 함수들입니다.
 task_sender.h
 ble_task.c - BLE 전송 요청 이벤트 관리
 test - ble task발 이벤트 수신을 위해 임시로 사용
 test_task.c
 task.c - Task init
 task.h
 app.c - App 레이어 init
 app.h
 hw - BSP, 드라이버 등
 main.c - app_main Task

BLE Task

BLE Task에서는 BLE와 관련된 역할을 수행합니다.
BLE 스택 실행
GAP/GATT 이벤트 처리 및 다른 Task로 이벤트 전송
다른 Task에서 이벤트 수신해 BLE 장치로 전송
ESP-IDF Bluetooth API Docs 중 일부 (바로가기)
ESP-IDF는 BLE 스택으로 Classic과 LE 모두 지원하는 BluedroidLE만을 지원하는 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 데이터나 게임패드 입력 데이터는 비트 단위로 파싱을 하는데요, 비트 연산자를 사용해도 되지만 조금 더 멋지고 아름답게(?) 처리하기 위해 structbit 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 구현으로 넘어가도록 하죠!