들어가며
FingerScore는 라켓 스포츠(테니스, 배드민턴, 탁구, 스쿼시)를 위한 스마트 점수기록 플랫폼입니다. 손가락에 끼는 작은 BLE 링이 있고, 간단한 제스처(손가락을 가볍게 톡톡 치거나 버튼을 누르는 동작)로 점수를 올리면 그 정보가 무선으로 폰이나 웹 앱에 전달됩니다. 경기 중에 점수판으로 달려가거나 종이에 적을 필요 없이, 그냥 손가락 동작 한 번으로 점수가 기록되는 것이죠.
이 시리즈에서는 그 하드웨어를 실제로 만들 수 있도록 단계별로 가르칩니다. 1~3편에서는 마이크로컨트롤러 선택, 센서(가속도계로 제스처 감지), 펌웨어 기초를 다뤘습니다. 이번 4편의 주제는 **무선 통신**입니다. 링이 아무리 똑똑하게 제스처를 감지해도, 그 신호를 폰까지 안정적으로 보내지 못하면 아무 소용이 없습니다.
왜 하필 BLE(Bluetooth Low Energy)일까요? 왜 Wi-Fi나 일반 클래식 블루투스가 아닐까요? 어떻게 "점수가 1 올랐다"는 정보를 폰에 전달할까요? 페어링은 안전한가요? 이 글에서 그 모든 것을 처음부터 설명합니다. 전자공학 전공자가 아니어도 따라올 수 있도록, 비유와 구체적인 코드 골격을 함께 제공합니다.
1. 왜 BLE인가 — 저전력의 철학
무선 통신 후보들
손가락에 끼는 링은 배터리가 아주 작습니다. 코인셀(CR2032) 하나, 혹은 손톱만 한 LiPo 배터리가 전부죠. 그래서 무선 방식을 고를 때 가장 중요한 기준은 **전력**입니다.
| 방식 | 평균 소비전력 | 거리 | 데이터량 | FingerScore 적합성 |
| --- | --- | --- | --- | --- |
| Wi-Fi | 매우 높음(수백 mA) | 길다 | 매우 큼 | 부적합(배터리 몇 시간) |
| 클래식 Bluetooth | 높음(수십 mA) | 중간 | 큼 | 부적합 |
| BLE | 매우 낮음(평균 수십 uA) | 중간(10m 내외) | 작음(점수 몇 바이트) | 최적 |
| ANT+ | 낮음 | 중간 | 작음 | 가능하나 폰 지원 약함 |
| LoRa | 낮음 | 매우 길다 | 매우 작음 | 과함(거리 불필요) |
FingerScore가 보내는 데이터는 "점수가 1 올랐다", "세트가 끝났다" 같은 아주 작은 이벤트입니다. 한 번에 몇 바이트면 충분하죠. 큰 데이터를 빠르게 보낼 필요가 전혀 없습니다. 반면 배터리는 며칠에서 몇 주를 버텨야 합니다. 이 조건에 가장 잘 맞는 것이 BLE입니다.
BLE는 어떻게 전력을 아끼나
핵심 아이디어는 **"대부분의 시간 동안 자고, 아주 잠깐만 깬다"**입니다. BLE 무선 칩은 평소에는 거의 꺼져 있다가, 정해진 짧은 시간(connection interval)에만 깨어나 데이터를 주고받습니다. 깨어 있는 시간이 1%도 안 되는 경우가 많습니다.
BLE 라디오의 시간 사용 (개념도)
전력
^
| . . . .
| | | | | <- 짧은 송수신 구간 (수 ms)
| | | | |
|___|_________|_________|_________|____> 시간
<--------> <- 대부분 sleep (수십~수백 ms)
interval
이 "잠깐 깨어나는 주기"를 connection interval이라고 하며, 뒤에서 자세히 다룹니다. 이 주기를 길게 잡을수록 전력은 절약되지만 반응 속도는 느려집니다. 트레이드오프죠.
2. BLE의 두 기둥 — GAP과 GATT
BLE를 처음 배울 때 가장 헷갈리는 게 GAP과 GATT라는 두 약어입니다. 비유로 정리해 봅시다.
- **GAP(Generic Access Profile)**: "누가 어떻게 연결되는가"를 다루는 규칙. 파티에서 서로를 발견하고 인사하고 짝을 짓는 단계라고 생각하세요.
- **GATT(Generic Attribute Profile)**: "연결된 다음 무슨 데이터를 어떻게 주고받는가"를 다루는 규칙. 인사가 끝난 뒤 실제로 나누는 대화의 형식입니다.
GAP — 발견과 연결
BLE 기기에는 보통 두 가지 역할이 있습니다.
- **Peripheral(주변기기)**: 자기 존재를 광고(advertising)하는 쪽. FingerScore 링이 여기에 해당합니다.
- **Central(중심기기)**: 광고를 스캔하고 연결을 거는 쪽. 보통 폰이 여기에 해당합니다.
링은 주기적으로 "나 여기 있어요, FingerScore 링이에요"라는 작은 패킷(advertising packet)을 방송합니다. 폰은 주변을 스캔하다가 이 패킷을 발견하면 연결을 시도합니다.
[FingerScore 링] --- advertising ---> (공중) <--- scanning --- [폰]
Peripheral Central
발견 후:
[링] <========= connection established =========> [폰]
광고 패킷에는 기기 이름, 서비스 UUID(아래에서 설명), 송신 출력 등을 담을 수 있습니다. 다만 크기 제한(레거시 광고 기준 31바이트)이 있어 꼭 필요한 것만 넣어야 합니다.
GATT — 데이터의 구조
연결이 되면 이제 GATT의 세계입니다. GATT는 데이터를 **서비스(Service) → 캐릭터리스틱(Characteristic) → 디스크립터(Descriptor)**라는 계층으로 정리합니다.
- **서비스(Service)**: 관련된 데이터의 묶음. 예) "점수 서비스", "배터리 서비스".
- **캐릭터리스틱(Characteristic)**: 실제 데이터 한 조각. 예) "현재 점수", "세트 번호", "배터리 잔량".
- **디스크립터(Descriptor)**: 캐릭터리스틱에 대한 메타데이터. 예) 사람이 읽을 설명, 알림 설정.
비유하자면 서비스는 서랍장, 캐릭터리스틱은 서랍 하나, 디스크립터는 서랍에 붙은 라벨입니다.
GATT 서버 (FingerScore 링) 구조
Service: Battery Service (표준, UUID 0x180F)
└─ Characteristic: Battery Level (0x2A19) [Read, Notify]
Service: FingerScore Score Service (커스텀 128-bit UUID)
├─ Characteristic: Score State [Read, Notify]
├─ Characteristic: Match Control [Write]
└─ Characteristic: Device Config [Read, Write]
여기서 중요한 점: **Peripheral(링)이 GATT 서버**, **Central(폰)이 GATT 클라이언트**가 되는 경우가 일반적입니다. 즉 데이터를 보관하고 제공하는 쪽이 링입니다.
3. 캐릭터리스틱의 동작 — Read, Write, Notify
캐릭터리스틱은 몇 가지 속성(property)을 가집니다. FingerScore에서 가장 중요한 세 가지를 봅시다.
| 속성 | 방향 | 의미 | FingerScore 예시 |
| --- | --- | --- | --- |
| Read | 폰이 링에게 요청 | 폰이 값을 읽어감 | 현재 점수 한 번 조회 |
| Write | 폰이 링에게 보냄 | 폰이 값을 씀 | "경기 리셋" 명령 전송 |
| Notify | 링이 폰에게 밀어줌 | 값이 바뀌면 링이 알림 | 점수가 오르면 즉시 통보 |
FingerScore의 핵심은 **Notify**입니다. 폰이 계속 "점수 바뀌었어?"라고 물어보는(polling) 방식은 전력 낭비가 큽니다. 대신 점수가 실제로 바뀌었을 때만 링이 폰에게 "방금 바뀌었어!"라고 알려주는 게 효율적이죠.
Notify가 동작하는 원리
폰은 연결 후, 알림을 받고 싶은 캐릭터리스틱의 특수 디스크립터인 **CCCD(Client Characteristic Configuration Descriptor)**에 값을 씁니다. 이 값을 1로 쓰면 "알림 켜줘"라는 뜻입니다. 그 뒤로 링이 점수 캐릭터리스틱 값을 갱신할 때마다 자동으로 폰에 전달됩니다.
1. 폰: Score State 캐릭터리스틱의 CCCD에 0x0001 Write (알림 구독)
2. 링: 제스처 감지 -> 점수 +1 -> Score State 값 갱신
3. 링: 갱신된 값을 즉시 폰에 Notify 전송
4. 폰: 콜백으로 새 점수 수신 -> UI 갱신
참고로 Notify와 비슷한 Indicate가 있습니다. 차이는 **확인 응답(ACK)** 여부입니다. Notify는 보내고 끝(빠르지만 유실 가능), Indicate는 폰이 받았다고 답을 줘야 다음을 보냅니다(느리지만 신뢰성↑). 점수처럼 누락되면 안 되는 데이터는 Indicate가 안전할 수 있으나, 보통은 Notify에 시퀀스 번호를 붙여 유실을 감지하는 방식을 씁니다. 이건 4절 패킷 설계에서 다룹니다.
4. 커스텀 GATT 서비스 설계 — 점수 이벤트
표준 서비스(배터리, 심박 등)는 UUID가 정해져 있지만, FingerScore의 점수 서비스는 우리만의 것이므로 **128비트 커스텀 UUID**를 직접 만들어 씁니다. UUID 생성기로 무작위 UUID를 하나 뽑아 베이스로 삼고, 서비스/캐릭터리스틱마다 일부 바이트만 바꿔 쓰는 게 관례입니다.
Base UUID: 6E40XXXX-B5A3-F393-E0A9-E50E24DCCA9E (예시)
Score Service : 6E400001-...
├ Score State char : 6E400002-... [Notify, Read]
├ Match Control char: 6E400003-... [Write]
└ Device Config char: 6E400004-... [Read, Write]
점수 패킷 설계
"점수가 올랐다"를 어떻게 바이트로 표현할까요? 너무 단순하게 점수 숫자 하나만 보내면, 패킷이 유실됐을 때 폰이 알 방법이 없습니다. 그래서 작지만 똑똑한 패킷 구조를 설계합니다.
Score State 패킷 (8 bytes)
Offset Size Field 설명
0 1 seq 시퀀스 번호(0~255 순환). 유실 감지용
1 1 event_type 0=점수, 1=세트종료, 2=경기종료, 3=리셋
2 1 player 0=나, 1=상대
3 1 score_self 내 현재 점수
4 1 score_oppo 상대 현재 점수
5 1 set_self 내 세트 수
6 1 set_oppo 상대 세트 수
7 1 battery_pct 배터리 잔량(%)
이 설계의 장점:
- **시퀀스 번호**로 폰이 "5번 다음에 7번이 왔네? 6번 유실됐다"를 감지할 수 있습니다. 그러면 폰이 Read로 현재 상태를 다시 읽어와 동기화합니다.
- 점수를 누적값(현재 점수)으로 보내므로, 한두 개 패킷이 유실돼도 다음 패킷으로 자동 복구됩니다(증분이 아니라 절댓값이라 안전).
- 8바이트는 BLE 기본 MTU(23바이트, 실제 페이로드 20바이트) 안에 충분히 들어갑니다.
> 설계 원칙: 작은 IoT 기기에서는 "증분(+1)"보다 "현재 상태 전체"를 보내는 게 유실에 강합니다. 패킷 하나가 사라져도 다음 패킷이 진실을 담고 있으니까요.
5. 펌웨어 코드 골격 (Peripheral 측)
실제 칩(예: Nordic nRF52, ESP32)마다 SDK가 다르지만, 개념은 비슷합니다. 여기서는 의사코드에 가까운 C 골격으로 흐름을 보여줍니다. 중괄호와 포인터는 모두 코드 펜스 안에 있으니 안전합니다.
// 점수 상태를 담는 구조체
typedef struct {
uint8_t seq;
uint8_t event_type;
uint8_t player;
uint8_t score_self;
uint8_t score_oppo;
uint8_t set_self;
uint8_t set_oppo;
uint8_t battery_pct;
} score_packet_t;
static score_packet_t g_score;
static uint16_t score_char_handle; // GATT 캐릭터리스틱 핸들
// 제스처 감지 콜백 (가속도계 인터럽트에서 호출)
void on_gesture_score(uint8_t which_player) {
g_score.seq++; // 시퀀스 증가
g_score.event_type = 0; // 점수 이벤트
g_score.player = which_player;
if (which_player == 0) g_score.score_self++;
else g_score.score_oppo++;
g_score.battery_pct = battery_read_percent();
// 캐릭터리스틱 값 갱신 + Notify 전송
ble_notify(score_char_handle,
(uint8_t*)&g_score,
sizeof(g_score));
}
핵심은 제스처 인터럽트가 들어오면 구조체를 갱신하고 `ble_notify()`를 호출하는 것뿐입니다. 평소에 MCU는 슬립 상태로 있다가, 가속도계가 "톡!" 하는 충격을 감지하면 인터럽트로 깨어나 이 함수를 실행하고 다시 잠듭니다.
Write 처리 (폰 → 링 명령)
폰이 "경기 리셋" 같은 명령을 보내면 Write 콜백이 호출됩니다.
// Match Control 캐릭터리스틱에 Write가 들어올 때
void on_match_control_write(const uint8_t *data, uint16_t len) {
if (len < 1) return;
uint8_t cmd = data[0];
switch (cmd) {
case 0x01: // 경기 리셋
memset(&g_score, 0, sizeof(g_score));
break;
case 0x02: // 세트 시작
g_score.score_self = 0;
g_score.score_oppo = 0;
break;
default:
break; // 알 수 없는 명령 무시
}
}
6. 페어링과 보안 — LE Secure Connections
남의 폰이 내 점수를 가로채거나, 가짜 링이 폰에 붙으면 곤란합니다. BLE는 이를 위한 보안 절차를 제공합니다.
보안의 3단계
1. **Pairing(페어링)**: 두 기기가 서로 임시 키를 교환해 암호화 채널을 만드는 과정.
2. **Bonding(본딩)**: 페어링 후 키를 저장해, 다음에 다시 만나면 자동으로 신뢰하는 것.
3. **Encryption(암호화)**: 실제 데이터를 암호화해서 도청을 막는 것.
최신 BLE(4.2 이상)는 **LE Secure Connections**를 지원합니다. 이는 ECDH(타원곡선 디피-헬만) 키 교환을 사용해, 중간자(MITM)가 패킷을 다 엿봐도 키를 알아낼 수 없게 합니다. 구형의 LE Legacy Pairing보다 훨씬 안전합니다.
페어링 방식 선택
FingerScore 링에는 디스플레이도 키패드도 없습니다. 이런 기기는 보통 다음 중 하나를 씁니다.
| 방식 | 필요 UI | MITM 방어 | FingerScore |
| --- | --- | --- | --- |
| Just Works | 없음 | 약함 | 기본 사용(편의 우선) |
| Passkey Entry | 숫자 입력 | 강함 | UI 없어 불가 |
| Numeric Comparison | 화면 2개 | 강함 | UI 없어 불가 |
| OOB | 별도 채널(NFC 등) | 강함 | NFC 추가 시 가능 |
링에는 UI가 없으니 보통 **Just Works**로 시작하되, 점수 데이터는 민감도가 낮으므로 실용적으로 괜찮습니다. 다만 펌웨어 업데이트 같은 민감한 동작에는 추가 인증(앱 계정 기반 토큰 등)을 얹는 게 좋습니다.
7. 모바일·웹 연동
Web Bluetooth (웹 앱)
크롬 계열 브라우저는 Web Bluetooth API로 BLE 기기에 직접 접근할 수 있습니다. 사용자가 버튼을 눌러 명시적으로 기기를 선택해야만 연결됩니다(보안상).
const SCORE_SERVICE = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const SCORE_CHAR = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
async function connectRing() {
// 사용자에게 기기 선택 팝업 표시
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [SCORE_SERVICE] }],
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(SCORE_SERVICE);
const ch = await service.getCharacteristic(SCORE_CHAR);
// Notify 구독
await ch.startNotifications();
ch.addEventListener('characteristicvaluechanged', (e) => {
const v = e.target.value; // DataView
const seq = v.getUint8(0);
const scoreSelf = v.getUint8(3);
const scoreOppo = v.getUint8(4);
updateScoreboard(seq, scoreSelf, scoreOppo);
});
}
Android (Kotlin) 개요
안드로이드 네이티브는 `BluetoothGatt` API를 씁니다. 콜백 기반이라 흐름이 비슷합니다.
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
g.discoverServices() // 서비스 탐색 시작
}
}
override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
val ch = g.getService(SCORE_SERVICE_UUID)
.getCharacteristic(SCORE_CHAR_UUID)
g.setCharacteristicNotification(ch, true)
// CCCD에 알림 활성화 값 쓰기
val cccd = ch.getDescriptor(CCCD_UUID)
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
g.writeDescriptor(cccd)
}
override fun onCharacteristicChanged(g: BluetoothGatt, ch: BluetoothGattCharacteristic) {
val data = ch.value
val scoreSelf = data[3].toInt() and 0xFF
val scoreOppo = data[4].toInt() and 0xFF
// UI 스레드에서 점수판 갱신
}
}
iOS는 CoreBluetooth(`CBCentralManager`, `CBPeripheral`)로 거의 동일한 개념을 다룹니다.
8. 전력 절약 — connection interval과 패킷 빈도
연결이 유지되는 동안에도 전력을 더 아낄 수 있습니다. 핵심 파라미터는 다음과 같습니다.
| 파라미터 | 의미 | 트레이드오프 |
| --- | --- | --- |
| Connection Interval | 깨어나 통신하는 주기(7.5ms~4s) | 짧으면 빠름·전력↑, 길면 느림·전력↓ |
| Slave Latency | 보낼 게 없으면 건너뛸 수 있는 횟수 | 높이면 전력↓, 반응성은 유지 |
| Supervision Timeout | 이 시간 응답 없으면 연결 끊김 판단 | 너무 짧으면 오작동, 길면 끊김 감지 느림 |
FingerScore는 점수가 자주 바뀌지 않습니다(몇 초~몇십 초에 한 번). 그래서 **긴 connection interval + 높은 slave latency** 조합이 이상적입니다. 평소엔 거의 자다가, 점수가 바뀐 순간 Notify로 깨어나면 됩니다.
전력 vs 반응성 (개념)
interval 짧음(15ms) : 즉각 반응, 배터리 며칠
interval 중간(100ms) : 거의 즉각, 배터리 1~2주
interval 김(500ms) : 0.5초 지연, 배터리 몇 주
점수 기록은 0.1~0.5초 지연이 문제 안 됨 -> 긴 interval 선택
펌웨어에서 연결 후 `connection parameter update`를 요청해 폰에게 "더 느긋한 주기로 바꾸자"고 제안할 수 있습니다.
9. 안테나와 전파 — 2.4GHz와 인체
BLE는 2.4GHz 대역을 씁니다. 이 주파수는 물에 잘 흡수되는데, **사람 몸의 대부분이 물**입니다. 링은 손가락에 끼므로 안테나가 피부에 매우 가깝죠. 이게 통신 거리를 줄이는 주요 원인입니다.
대응 방법:
- **안테나 배치**: 손가락 안쪽(살에 닿는 면)이 아니라 바깥쪽(공기 쪽)으로 안테나가 향하게 PCB를 배치합니다.
- **그라운드 키프아웃**: 안테나 주변에 구리(그라운드)를 비워 둬야 방사가 잘 됩니다. 이건 6편 PCB 설계에서 깊이 다룹니다.
- **출력 조절**: 송신 출력(TX power)을 적절히 높이되, 너무 높이면 배터리를 갉아먹습니다.
인체 영향에 대해선, BLE의 송신 출력은 매우 낮습니다(보통 0dBm = 1mW 수준, 휴대폰 통화보다 훨씬 약함). 일반적인 사용에서 건강 문제는 보고된 바 없으며, 규제 기관의 SAR(전자파 흡수율) 기준 안에서 동작합니다. 다만 인증(FCC/KC)은 6편에서 다룹니다.
10. 디버깅 — 보이지 않는 무선을 보는 법
무선은 눈에 안 보여서 디버깅이 어렵습니다. 다행히 좋은 도구들이 있습니다.
- **nRF Connect (모바일 앱)**: 폰에서 주변 BLE 기기를 스캔하고, 서비스·캐릭터리스틱을 펼쳐 보고, 직접 Read/Write/Notify를 시험할 수 있습니다. 펌웨어 개발 초기에 필수입니다. 폰 앱이 없어도 이걸로 링의 GATT 구조가 제대로 만들어졌는지 확인합니다.
- **BLE 스니퍼**: nRF52840 동글 + Wireshark 조합으로 공중의 패킷을 직접 캡처합니다. 연결이 왜 끊기는지, 어느 패킷이 유실되는지 등 깊은 문제를 추적할 때 씁니다.
- **로깅(RTT/UART)**: 칩에서 시리얼 로그를 뽑아 펌웨어 내부 상태를 봅니다.
디버깅 단계별 도구
1단계: nRF Connect 앱으로 GATT 구조 확인 (서비스 보이나?)
2단계: nRF Connect로 Notify 구독 -> 제스처 시 값 바뀌나?
3단계: 안 되면 RTT 로그로 펌웨어 내부 확인
4단계: 그래도 안 되면 스니퍼로 공중 패킷 캡처
11. 흔한 함정
실제로 BLE 펌웨어를 만들면 거의 모두가 한 번씩 밟는 지뢰들입니다.
- **연결이 자꾸 끊긴다**: Supervision timeout이 너무 짧거나, 링이 슬립에 들어가며 라디오를 너무 오래 꺼서 응답을 놓치는 경우. 슬립과 connection interval의 균형을 맞춰야 합니다.
- **MTU 문제**: 기본 MTU는 23바이트(실 페이로드 20). 더 큰 데이터를 보내려면 연결 후 MTU 협상을 해야 합니다. FingerScore 8바이트 패킷은 협상 없이도 충분하니, 일부러 패킷을 작게 유지한 설계가 빛을 발합니다.
- **CCCD 안 켜고 Notify 기대**: 폰이 알림 구독(CCCD 쓰기)을 안 했는데 값만 갱신하면 폰은 아무것도 못 받습니다. 초보자가 가장 많이 막히는 지점입니다.
- **광고를 너무 자주/드물게**: 광고 주기가 너무 길면 폰이 링을 늦게 발견하고, 너무 짧으면 배터리를 낭비합니다. 보통 100ms~1s 사이에서 절충합니다.
- **본딩 정보 꼬임**: 폰에서 페어링을 지웠는데 링은 기억하고 있으면 재연결이 안 됩니다. 링에 "본딩 초기화" 동작(예: 길게 누르기)을 넣어 두면 좋습니다.
12. 마치며 — 다음 글 예고
이번 4편에서는 FingerScore의 무선 신경망인 BLE를 처음부터 설계했습니다. GAP/GATT의 구조, Notify 기반 점수 전달, 유실에 강한 패킷 설계, LE Secure Connections 보안, Web Bluetooth와 Android 연동, 그리고 전력 절약과 디버깅까지 짚었습니다. 이제 링은 점수를 똑똑하게, 또 아껴 가며 폰에 보낼 수 있습니다.
하지만 여기엔 큰 전제가 하나 있습니다. **링에 전원이 들어와 있어야 한다**는 것이죠. 손가락만 한 기기 안에 어떻게 배터리를 넣고, 며칠을 버티게 하고, 안전하게 충전할까요?
다음 5편 **전원·배터리·회로 설계**에서는 전력 예산 계산부터 배터리 선택(코인셀 vs LiPo), 충전 IC와 보호회로, LDO와 벅 컨버터, 그리고 발열·ESD·안전까지 — 링을 진짜로 켜 두는 모든 것을 다룹니다.
참고 자료
- Bluetooth Core Specification — [https://www.bluetooth.com/specifications/specs/](https://www.bluetooth.com/specifications/specs/)
- Nordic Semiconductor nRF Connect SDK 문서 — [https://docs.nordicsemi.com/](https://docs.nordicsemi.com/)
- Web Bluetooth API (MDN) — [https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API)
- Android BLE 가이드 — [https://developer.android.com/develop/connectivity/bluetooth/ble/ble-overview](https://developer.android.com/develop/connectivity/bluetooth/ble/ble-overview)
- Apple Core Bluetooth — [https://developer.apple.com/documentation/corebluetooth](https://developer.apple.com/documentation/corebluetooth)
- nRF Connect for Mobile — [https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-mobile](https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-mobile)
- Adafruit Bluefruit LE 학습 자료 — [https://learn.adafruit.com/introduction-to-bluetooth-low-energy](https://learn.adafruit.com/introduction-to-bluetooth-low-energy)
- ESP32 BLE (ESP-IDF) 문서 — [https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/index.html](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/index.html)
현재 단락 (1/230)
FingerScore는 라켓 스포츠(테니스, 배드민턴, 탁구, 스쿼시)를 위한 스마트 점수기록 플랫폼입니다. 손가락에 끼는 작은 BLE 링이 있고, 간단한 제스처(손가락을 가볍게 ...