BLE 연결
S큐브는 Nordic UART Service(NUS)를 광고합니다. 각 큐브가 호스트에 직접 연결되며(Peripheral), 릴레이·멀티롤은 없습니다. 대상 펌웨어 v0.0.45.0.
GATT UUID
| 구분 | UUID | 속성 | 방향 |
|---|---|---|---|
| Service (NUS) | 6e400001-b5a3-f393-e0a9-e50e24dcca9e | — | — |
| 명령 특성 | 6e400002-b5a3-f393-e0a9-e50e24dcca9e | Write / WriteWithoutResponse | 호스트→큐브 |
| 데이터 특성 | 6e400003-b5a3-f393-e0a9-e50e24dcca9e | Notify | 큐브→호스트 |
G큐브 문서와 같은 기준입니다: ...0002에 명령을 쓰고, ...0003의 Notify로 데이터를 받습니다.
기기 탐색(스캔)
광고 이름은 SCUBE.XX.AIR입니다. XX는 그룹번호(16진 2자리, 자리마다 0~9, 출고 기본 00)입니다.
// Web Bluetooth 스캔 필터 예
filters: [{ namePrefix: "SCUBE." }] // 전체
filters: [{ name: "SCUBE.23.AIR" }] // 그룹 23만
optionalServices: ["6e400001-b5a3-f393-e0a9-e50e24dcca9e"]
연결 후 표준 시퀀스
- NUS 연결 →
...0003Notify 구독 — 구독 즉시 20바이트 센서 레코드가 200Hz로 흐릅니다. - 구독 직후 큐브가 통보 2개를 보냅니다: 현재 주기 0xF7, 확장 센서 종류 0xFF.
- 여러 큐브를 쓰면 연결 순서대로
[0x10][n](n=1~3)으로 순번을 지정합니다. - 필요 시 펌웨어 정보
[0x2D], 그룹번호 조회[0x23].
[명령ID 1B][인자…]이며, 알 수 없는 명령은 무시됩니다.수신 프레임 구분 — 길이로 판별
| Notify 길이 | 종류 | 처리 |
|---|---|---|
| 20의 배수 | 센서 레코드 | 20바이트 단위로 잘라 해석 (현재는 알림 1회 = 레코드 1개) |
| 그 외 | 통보·응답 프레임 | 첫 바이트가 프레임 ID (0xEC~0xFF) |
[6]은 연결 순번에 따라 값이 바뀌므로 프레임 식별에 쓰지 마세요. 항상 길이로 구분합니다.센서 레코드 (20바이트)
구독 중 큐브가 연속으로 보내는 기본 데이터입니다. 모든 다바이트 값은 Little Endian입니다. 20바이트는 BLE 무선 슬롯 하나에 정확히 맞는 크기입니다.
바이트 맵
| 바이트 | 필드 | 타입 | 설명 |
|---|---|---|---|
[0:5] | gyro x·y·z | int16 ×3 | 자이로 원시값 |
[6] | OP + 순번 | uint8 | 0x77=미지정 · 0x78=1번 · 0x79=2번 · 0x7A=3번 |
[7:8] | ext 대표값 | uint16 | 확장 포트 값. 아날로그면 12bit 원시값(0~4095), I2C 센서면 그 센서의 대표값 (「확장 센서」 탭) |
[9:10] | ext 보조값 | uint8 ×2 | 다중값 센서의 추가 값. 그 외에는 0 |
[11:16] | accel x·y·z | int16 ×3 | 가속도 원시값 |
[17] | prox | uint8 | 근접센서 반사 세기 0~255 (평상시 ≈0, 가리면 커짐) |
[18] | button + battery | bit7:6 / bit5:0 | 버튼 = r[18] >> 6 (0=없음 1=눌림 2=뗌) · 배터리 % = round((r[18] & 0x3F) × 100 ÷ 63) |
[19] | checksum | uint8 | CRC16-CCITT(rec[0:18])의 하위 1바이트 (poly 0x1021, init 0xFFFF) |
눈금 변환
| 센서 | 범위 | 변환 |
|---|---|---|
| 자이로 | ±2000 dps | dps = raw ÷ 16.384 |
| 가속도 (기본) | ±4 g | g = raw ÷ 8192 |
| 가속도 | ±16 g | g = raw ÷ 2048 |
가속도 범위는 [0x1B]로 바꾸며, 연결이 끊기면 ±4 g로 돌아갑니다.
갱신 주기
| 필드 | 측정 주기 |
|---|---|
| gyro · accel | 레코드마다 (기본 5ms = 200Hz, 동시 측정) |
| prox | 약 10Hz (최신값 유지) |
| battery | 6초마다 + 평활 |
| ext | 센서마다 다름 (최신값 유지) |
버튼 이벤트
- 눌림(1)·뗌(2)이 각각 정확히 한 번 실립니다. 눌린 동안 반복되지 않습니다.
- 버튼 이벤트 시 레코드가 추가로 즉시 나갈 수 있어, 도착 간격이 항상 일정하지는 않습니다.
- 2초 길게 누르면 전원이 꺼지고 연결이 끊깁니다(별도 이벤트 없음).
파싱 예제 (Python)
import struct
def parse_record(r: bytes):
gx, gy, gz = struct.unpack_from('<hhh', r, 0)
cube_no = r[6] - 0x77 # 0=미지정, 1~3
ext, = struct.unpack_from('<H', r, 7)
ax, ay, az = struct.unpack_from('<hhh', r, 11)
return {
'gyro_dps': (gx/16.384, gy/16.384, gz/16.384),
'accel_g': (ax/8192, ay/8192, az/8192), # ±4g 기준
'cube': cube_no, 'ext': ext, 'ext1': r[9], 'ext2': r[10],
'prox': r[17],
'button': r[18] >> 6,
'battery': round((r[18] & 0x3F) * 100 / 63),
}
def on_notify(data: bytes):
if len(data) % 20 == 0: # 센서 레코드
for i in range(0, len(data), 20):
print(parse_record(data[i:i+20]))
else: # 통보 프레임
print('frame', hex(data[0]), data.hex(' '))
명령·프레임 목록
호스트→큐브 명령(CMD)과 큐브→호스트 통보 프레임(FRAME) 전체입니다. 진단용 명령은 「진단·성능」 탭에 있습니다.
명령 (호스트 → 큐브)
| ID | 이름 | 인자 | 응답 | 분류 |
|---|---|---|---|---|
| 0x10 | 연결 순번 지정 | [n] 1~3 | — | 연결 |
| 0x11 | 샘플 주기 | [ms] 5~100 | 0xF7 | 스트리밍 |
| 0x12 | 연결 간격 요청 | [1.25ms 단위] 6~12 | — | 스트리밍 |
| 0x1B | 가속도 범위 | [4|16] | 0xF6 | 스트리밍 |
| 0x1C | 자동 감속 끔/켬 | [0|1] | 0xF7 | 스트리밍 |
| 0x24 | I2C 센서 조회/재검사 | 없음 / [1] | 0xFF | 확장 센서 |
| 0x25 | 확장 포트 값 선택 | [n] 0~8 / 없음=조회 | 0xFF | 확장 센서 |
| 0xEE | 확장 센서 상세값 | 없음 | 0xEE | 확장 센서 |
| 0x1A | 효과음 재생 | [id]([vol]) | — | 출력 |
| 0x2B | 임의 주파수 톤 | [Hz u16][ms u16]([vol]) | — | 출력 |
| 0x26 | LED 매트릭스 그리기 | [행0..행7]([밝기]) | — | 출력 |
| 0x2C | 인터랙션 모듈 LED | [비트마스크] | 레코드 [8] | 출력 |
| 0x27 | 스텝모터 각도 이동 | [각도 i16][속도 1~10] | 0xEF | 출력 |
| 0x28 | 스텝모터 연속 회전/정지 | [속도 i8] / 없음=정지 | 0xEF | 출력 |
| 0x23 | 그룹번호 지정/조회 | [BCD] / 없음=조회 | 0xFE | 시스템 |
| 0x29 | 원격 전원 OFF | [0x5A] | 연결 끊김 | 시스템 |
| 0x2D | 펌웨어 정보 | 없음 | 0xEC | 시스템 |
| 0x20 | 소리 녹음 (1.28초) | [AIN 0~3]([flags]) | 0xFA + 0xF9 | 소리 입력 |
| 0x21 | 음성 모드 진입/해제 | [0|1] | 0xFC | 소리 입력 |
| 0x22 | 연속 소리 스트리밍 | [ch]([형식]) / 없음=정지 | 0xFD + 0xF9 | 소리 입력 |
통보 프레임 (큐브 → 호스트)
| ID | 이름 | 길이 | 구조 |
|---|---|---|---|
| 0xEC | 펌웨어 정보 | 18B | [EC][형식][FW a.b.c.d][YY MM DD][SD FWID u16][IMU칩][BLE 주소 6B] |
| 0xEE | 확장 센서 상세값 | 2~10B | [EE][종류][값1 u16][값2 u16]… |
| 0xEF | 스텝모터 상태 | 4B | [EF][상태][남은 각도 i16] |
| 0xF6 | 가속도 범위 | 2B | [F6][범위 g] |
| 0xF7 | 주기 통보 | 3B | [F7][주기 ms][사유] |
| 0xF9 | 소리 데이터 | 19B | [F9][순번 u16][데이터 16B] |
| 0xFA | 녹음 요약 | 17B | 상태·샘플수·최소/최대·DC·RMS·표본율·형식 |
| 0xFC | 음성 모드 통보 | 3B | [FC][모드][사유] |
| 0xFD | 스트리밍 시작/종료 | 5B / 7B | 시작 [FD][1][ch][표본율 u16] · 종료 [FD][0][사유][프레임수 u32] |
| 0xFE | 그룹번호 | 4B | [FE][저장값][광고중 값][상태] |
| 0xFF | 확장 센서 종류 | 5B | [FF][종류][주소][대표값 u16] |
진단 응답 0xED · 0xF1~0xF5 · 0xF8 · 0xFB는 「진단·성능」 탭을 참조하세요.
확장 센서
I2C 센서는 부팅 시 자동 감지하고, 아날로그·1-Wire 센서는 호스트가 [0x25]로 선택합니다. 어떤 센서인지는 0xFF 프레임이 알려주며, 값은 레코드 [7:10]에 실립니다.
CMDI2C 센서 조회 0x24
| 보내기 | 동작 |
|---|---|
24 | 현재 감지 상태 조회 |
24 01 | 다시 찾기 (센서를 바꿔 꽂았을 때) |
재검사 동안(수십 ms) 레코드가 잠시 멈춥니다. 센서를 빼면 자동으로 감지 해제됩니다.
CMD확장 포트 값 선택 0x25
| n | 센서 | 종류 코드 |
|---|---|---|
| 0 | 선택 안 함 (I2C 자동 감지) | — |
| 1 | 방수온도 (DS18B20, 1-Wire) | 14 |
| 2 | 아날로그 원시값 | 15 |
| 3~8 | 빛 · 소리 · 볼륨 · 자석 · 초음파 · 인체감지 | 16~21 |
선택은 저장되어 재부팅 후에도 유지되고 즉시 적용됩니다. 인자 없이 보내면 조회.
FRAME확장 센서 종류 0xFF — 5바이트
연결 직후, 감지 상태가 바뀔 때, [0x24]·[0x25] 응답으로 옵니다. 이 프레임을 받기 전의 ext 값은 해석하지 마세요. 방수온도(14)는 주소 칸이 연결 여부(1=연결, 0=미연결)입니다.
종류 코드와 레코드 적재 규칙
| 종류 | 센서 (주소) | [7:8] 대표값 | [9] | [10] |
|---|---|---|---|---|
| 0 | 없음 | 아날로그 원시 0~4095 | 0 | 0 |
| 2 | 온도 (0x18) | 13bit 원시, ÷16 = ℃ | 0 | 0 |
| 3 | 컬러 VEML6040 (0x10) | R 상위8bit / G | B | C |
| 5 | SCD40 (0x62) | CO₂ ppm | 온도 ℃+45 | 습도 % |
| 7 | 인터랙션 모듈 TCA9534 (0x20) | [7] 버튼 0/1 · [8] 켜진 LED | 0 | 0 |
| 8 | SPS30 미세먼지 (0x69) | PM2.5 ㎍/㎥ | PM10 u16 | |
| 9 | LED 매트릭스 HT16K33 (0x70) | 출력 장치 — [0x26]로 그리기 | ||
| 10 | 큐브이노 (0x07) | 보드 ID | 0 | 0 |
| 11 | HuskyLens (0x32) | 인식된 대상 수 | 0 | 0 |
| 12 | BMP280 기압 (0x76/0x77) | 기압 0.1 hPa | 온도 i16 (0.01℃) | |
| 13 | INA219 전류 (0x40) | 전류 mA (i16) | 전압 0.1V | 전력 0.1W |
| 14 | 방수온도 DS18B20 (1-Wire) | 온도 0.1℃ (i16) | 0 | 0 |
| 15 | 아날로그 원시 (선택됨) | 0~4095 | 0 | 0 |
| 16~21 | 빛·소리·볼륨·자석·초음파·인체감지 | 0~4095 | 0 | 0 |
| 22 | BH1750 조도 (0x23/0x5C) | 조도 lx | 0 | 0 |
| 23 | TSL2561 조도 (0x39/0x29/0x49) | 조도 lx | 적외(CH1) u16 | |
| 24 | MAX30102 심박 (0x57) | 심박수 BPM (0=측정 전) | bit7 손가락 · bit0~6 SpO₂ % | 맥파 i8 |
1·4·6은 예약 번호입니다. 1바이트 칸은 정밀도를 줄인 값이므로 정밀값은 0xEE를 사용하세요. 심박·산소포화도는 교육용 추정값이며 의료용이 아닙니다.
CMD상세값 요청 0xEE → FRAME0xEE
요청할 때만 응답합니다. 응답은 [EE][종류][값1 u16 LE][값2 u16 LE]…이며, 값 개수는 종류가 정합니다. 상세값이 없는 센서는 [EE][종류] 2바이트만 옵니다.
| 센서 | 값 (순서대로) |
|---|---|
| 컬러 | R, G, B, C |
| SCD40 | CO₂ ppm, 온도 0.1℃, 습도 0.1% |
| SPS30 | PM1.0, PM2.5, PM4.0, PM10 |
| BMP280 | 기압 0.1 hPa, 온도 0.01℃ |
| INA219 | 전류 mA, 전압 mV, 전력 mW |
| BH1750 | 조도 lx |
| TSL2561 | 조도 lx, CH0(가시+적외), CH1(적외) |
| MAX30102 | 심박 BPM, SpO₂ %, IR 직류(÷16), 맥파 진폭 |
[0x25]로 아날로그 센서를 선택하세요.출력 장치
소리·LED 출력 명령은 응답이 없습니다. 스텝모터만 상태 통보 0xEF를 보냅니다.
CMD효과음 0x1A — 2~3바이트
[1A][id]([vol]) · vol = 1~50 (생략 시 8)
| id | 소리 |
|---|---|
| 0 | 시작 (1318Hz) |
| 1 | 저장 (659Hz) |
| 2 | 성공 (659→988Hz) |
| 3 | 거절 (220Hz) |
CMD임의 주파수 톤 0x2B — 5~6바이트
[2B][주파수 u16][길이 ms u16]([vol])
- 범위 50~4000Hz · 10~5000ms · vol 1~50
- 주파수
0= 즉시 정지
2B B8 01 F4 01 // 440Hz, 500ms
CMDLED 매트릭스 그리기 0x26 — 9~10바이트
[26][행0]…[행7]([밝기 0~15]) · 행0 = 맨 위, 각 행의 bit0 = 왼쪽 칸. 매트릭스(종류 9)가 감지된 경우에만 동작합니다.
26 00 66 FF FF 7E 3C 18 00 08 // 하트, 밝기 8
한 번 그리는 데 약 2ms가 걸리므로 연속 갱신은 100ms 정도 간격을 권장합니다.
CMD인터랙션 모듈 LED 0x2C — 2바이트
[2C][마스크] · bit0 = 빨강, bit1 = 초록, bit2 = 파랑 (1 = 켜짐, 8가지 색)
2C 00 // 끄기 2C 01 // 빨강
2C 06 // 청록 2C 07 // 흰색
응답 대신 현재 켜진 색이 레코드 [8]에 실립니다. 보낸 값과 다르면 다시 보내세요(재시도 횟수 제한 권장).
스텝모터 (본체)
CMD각도 이동 0x27 — 4바이트
[27][각도 i16][속도 1~10] · 현재 위치 기준 상대 각도(부호 = 방향) · 0.18°/스텝 · 속도 1~10 ≈ 3~30 RPM. 이동 중 새 명령을 보내면 새 명령으로 바뀝니다.
27 5A 00 05 // +90°, 속도 5
27 A6 FF 0A // −90°, 속도 10
CMD연속 회전 / 정지 0x28
[28][속도 i8] = 회전(부호 = 방향, 0 = 정지) · [28] = 정지. 정지하면 코일 전류가 꺼집니다(유지 토크 없음). 연결이 끊기면 자동 정지합니다.
FRAME스텝모터 상태 0xEF — 4바이트
[EF][상태][남은 각도 i16] · 상태 0 = 멈춤, 1 = 각도 이동 중, 2 = 연속 회전 중. 상태가 바뀔 때만 옵니다.
설정·시스템
CMD연결 순번 0x10 — 2바이트
[10][n] · n = 1(빨강), 2(초록), 3(파랑). 연결 LED 색과 레코드 [6](= 0x77 + n)에 반영됩니다. 연결이 끊기면 초기화됩니다.
CMD샘플 주기 0x11
[11][ms] · 5~100ms (5 = 200Hz 기본, 10 = 100Hz, 20 = 50Hz). 적용 시 0xF7 통보.
CMD연결 간격 0x12
[12][n] · n × 1.25ms (6 = 7.5ms, 12 = 15ms 기본). 호스트에 요청만 하며, 거절되면 기존 간격을 유지합니다. 연결 약 2초 뒤에 보내세요.
CMD가속도 범위 0x1B
[1B][4|16] → 0xF6 [F6][범위]. 응답을 받은 순간부터 새 눈금으로 해석하세요.
CMD자동 감속 0x1C
[1C][0|1] · 기본 켬. 전송이 밀리면 큐브가 스스로 주기를 낮추고(최저 100Hz) 0xF7로 알립니다. 200Hz가 꼭 필요하면 끄세요.
FRAME주기 통보 0xF7 — 3바이트
[F7][주기 ms][사유] · 사유 0 = 구독 시작, 1 = 명령 적용, 2 = 자동 감속, 3 = 자동 회복. 주기가 바뀌면 반드시 통보됩니다.
CMD그룹번호 0x23 → FRAME0xFE
[23][XX] = 지정 · [23] = 조회. 기기 이름 SCUBE.XX.AIR의 XX이며, 저장 후 다음 부팅부터 적용됩니다.
- XX는 16진 2자리이며 자리마다 0~9입니다. 그룹 “23” →
23 23전송. 0x0A처럼 A~F가 들어간 값은 거부됩니다.
응답 [FE][저장값][광고 중 값][상태] · 상태 0 = 조회, 1 = 저장 완료, 2 = 같은 값, 3 = 범위 밖, 4 = 쓰기 실패. 두 값이 다르면 재부팅이 필요합니다.
→ 23 23
← FE 23 00 01 // 23 저장, 현재 00으로 광고 중 → 재부팅 후 SCUBE.23.AIR
CMD원격 전원 OFF 0x29
29 5A — 확인 바이트 0x5A가 필요합니다. 버튼 2초 누름과 같이 꺼지며 연결이 끊깁니다. 그룹번호 지정 후 바로 적용할 때 유용합니다.
CMD펌웨어 정보 0x2D → 0xEC
2D → 18바이트 응답. 1초 안에 응답이 없으면 v0.0.43.0 이하 펌웨어입니다.
FRAME펌웨어 정보 0xEC — 18바이트
| 바이트 | 내용 |
|---|---|
[0] | 0xEC |
[1] | 형식 (현재 1). 길이가 18 이상이면 아는 필드까지만 읽으세요. |
[2:5] | 펌웨어 버전 4자리 (예 0.0.45.0 → 00 00 2D 00) |
[6:8] | 빌드 날짜 YY·MM·DD (이진값) |
[9:10] | SoftDevice FWID (S132 v6.0.0 = 0x00A8) |
[11] | IMU 칩 (0 = 미상, 1 = BMI160, 2 = BMI323) |
[12:17] | BLE 주소 (하위 바이트 먼저) |
소리 입력 (아날로그 마이크)
확장 포트의 소리센서(AIN3)로 8000Hz 소리를 받습니다. 소리 데이터는 0xF9(19바이트) 프레임으로 오며, 유실 없이 재전송됩니다.
CMD단발 녹음 0x20
[20][AIN 채널 0~3]([flags]) · 1.28초(10240샘플) 녹음 후 요약 0xFA 1개와 데이터 0xF9가 이어집니다. 소리센서는 채널 3입니다.
| flags | 뜻 |
|---|---|
| bit0 | 요약만 전송 |
| bit1 | 4비트 IMA ADPCM 압축 전송 (생략 시 16bit 원시) |
CMD음성 모드 0x21 · 연속 스트리밍 0x22
21 01→ 음성 모드 진입 (센서 레코드 전송 정지) · 0xFC[FC][1][사유]22 03→ 채널 3 연속 스트리밍 시작 · 0xFD 시작 통보 후 0xF9 연속22→ 정지 (남은 데이터 모두 전송 후 0xFD 종료 통보)21 00→ 센서 모드 복귀
| 형식 인자 | 압축 | 프레임당 표본 | 프레임/초 |
|---|---|---|---|
| 생략 (기본) | 4비트 ADPCM | 32 | 250 |
2 | 2비트 ADPCM | 64 | 125 |
스트리밍 중 큐브 버튼을 짧게 누르면 정지합니다. 연결이 끊기면 센서 모드로 돌아갑니다.
FRAME소리 데이터 0xF9
[F9][순번 u16][16바이트] · 원시 = 표본 8개, 4비트 = 32개, 2비트 = 64개. ADPCM은 앞 프레임에 의존하므로 순번으로 유실을 확인하세요.
FRAME스트리밍 0xFD
시작 [FD][1][ch][표본율 u16] · 종료 [FD][0][사유][보낸 프레임 수 u32]
종료 사유 0 = 명령, 1 = 버퍼 초과, 2 = 모드 해제, 3 = 시작 거부, 4 = 하드웨어 실패, 5 = 버튼
진단·성능
진단 명령 (개발·점검용)
| 명령 | 이름 | 응답 | 내용 |
|---|---|---|---|
| 0x13 | 전송 통계 | 0xF1 9B | [F1][생성 수 u32][버린 수 u32] (구독 시점부터 누적) |
| 0x16 | IMU 칩 판별 | 0xF3 5B | [F3][칩]… 1 = BMI160, 2 = BMI323 |
| 0x18 | 링크 상태 | 0xF4 7B | [F4][LL payload][ATT_MTU u16][연결 간격 u16][PHY] |
| 0x19 | 배터리 원시값 | 0xF5 6B | [F5][즉시 raw u16][평활 raw u16][%] · V = raw × 7.5 ÷ 4096 |
| 0x2A | IMU 영점 오프셋 | 0xED 16B | 2A 읽기 · 2A 01 영점 보정 후 읽기 (큐브를 평평히 놓고) |
| 0x14 | 레코드 크기 (계측) | — | 20~60B로 패딩 — 처리량 측정용 |
| 0x15 0x17 | 근접센서 진단·조정 | 0xF2 | 근접센서 핀 읽기·파라미터 조정 |
| 0x1D 0x1E 0x1F | 하드웨어 탐지 | 0xF8 · 0xFA · 0xFB | I2C 스캔 · PDM 녹음 · 핀 부착 탐지 (보드 개발용) |
진단 명령 중 일부는 실행 중 센서 레코드를 잠시 멈춥니다.
전송 성능 (Windows PC 실측)
| 구성 | 권장 설정 | 결과 |
|---|---|---|
| 큐브 1대 | 기본 (5ms, 200Hz) | 약 200 레코드/초 |
| 큐브 2대 | 연결 간격 7.5ms (12 06) + 각 100Hz (11 0A) | 유실 0 |
| 큐브 2대 | 연결 간격 15ms (기본) + 각 50Hz (11 14) | 유실 0 |
- 먼저 연결한 큐브가 대역을 적게 받는 경향이 있습니다. 동시에 여러 대를 쓰면 주기를 낮추세요.
- 전송 큐가 차면 오래된 센서 레코드를 버립니다(최신 우선). 통보·소리 프레임은 버리지 않습니다.
- 유실은
[0x13]통계로 확인합니다. 레코드에는 순번 필드가 없습니다.