안드로이드 전용. HC-06(SPP) 연결. 이 문서 하나로 앱이 완성되도록 썼습니다.
쓰는 법: 아래 「7단계 프롬프트」를 순서대로 하나씩 AI에게 넣습니다. 한 번에 다 넣으면 AI가 절반쯤에서 대충 넘어갑니다. 단계마다 코드를 받아 빌드해 보고 다음으로 갑니다. 각 단계 프롬프트는 회색 블록 안의 내용을 그대로 복사하면 됩니다.
먼저 §1 통신 규격과 §2 화면 설계를 읽고, 1단계 프롬프트에 §1을 통째로 붙여넣으세요. AI는 이 규격을 계속 참조해야 합니다.
| 항목 | 값 |
|---|---|
| 방식 | 고전 블루투스 SPP (Bluetooth Classic). BLE 아님 |
| 모듈 | HC-06 |
| 통신 속도 | 9600 bps, 8N1 |
| SPP UUID | 00001101-0000-1000-8000-00805F9B34FB |
| 페어링 | 앱이 직접 페어링하지 않음. 안드로이드 설정에서 미리 페어링된 기기 목록에서 고름 |
| HC-06 기본 PIN | 1234 또는 0000 |
| 인코딩 | ASCII, 줄 단위, 구분자 \n (CR이 섞여 올 수 있으니 \r은 버릴 것) |
250 ms 주기로 계속 들어옵니다.
D,<전방cm>,<낙하편차cm>,<소음>,<밝기>,<배터리>,<플래그>
실제 예시:
D,87,3,42,15,78,0 평소 상태
D,42,2,55,80,77,1 전방 42cm 경고 중
D,-1,999,38,12,76,18 앞은 비었고 아래도 허공 + 기준 미보정
| # | 필드 | 자료형 | 범위 | 의미 |
|---|---|---|---|---|
| 0 | 태그 | 문자 | D |
상태 패킷 표시 |
| 1 | 전방cm | int | −1, 2~400 | 정면 장애물 거리. −1 = 감지된 것 없음 |
| 2 | 낙하편차cm | int | −50~999 | 기준 지면 대비 벌어진 정도. 999 = 반사 없음(허공) |
| 3 | 소음 | int | 0~100 | 40 ms 창의 peak-to-peak |
| 4 | 밝기 | int | 0~100 | 0 = 캄캄 |
| 5 | 배터리 | int | 0~100 | 퍼센트 |
| 6 | 플래그 | int | 0~31 | 아래 비트합 |
플래그 비트
| 비트 | 값 | 상수명 | 의미 | 앱이 할 일 |
|---|---|---|---|---|
| 0 | 1 | FLAG_FRONT |
전방 경고 중 | 거리 숫자를 경고색으로 |
| 1 | 2 | FLAG_DROP |
낙하 경고 중 | 빨간 배너 + 폰 진동 |
| 2 | 4 | FLAG_MUTE |
무음모드 켜짐 | 토글 버튼 상태 동기화 |
| 3 | 8 | FLAG_SOS |
SOS 발동 중 | 전체화면 경고 |
| 4 | 16 | FLAG_UNCAL |
하방 기준 미보정 | "평지에서 보정하세요" 안내 배너 |
상태 패킷 사이에 끼어들어 옵니다. 형식은 E,<이름>.
| 메시지 | 언제 | 앱이 할 일 |
|---|---|---|
E,BOOT |
지팡이 전원이 켜졌을 때 | 저장해 둔 설정값을 다시 전송 |
E,SOS |
SOS 발동. 해제 전까지 3초마다 반복 | 최초 1회만 보호자 알림. 이후는 무시 |
E,PONG |
P 명령에 대한 응답 |
연결 살아있음 확인 |
E,OK |
설정 변경 성공 | 슬라이더 옆에 체크 표시 |
E,ZOK |
하방 기준 보정 완료 | "보정 완료" 토스트 |
E,SOSOFF |
SOS 해제됨 | 경고 화면 닫기 |
E,ERR |
알 수 없는 명령 | 로그만 남김 |
각 명령 끝에 \n을 붙여 보냅니다.
| 명령 | 인자 범위 | 예 | 의미 | 응답 |
|---|---|---|---|---|
S1 / S0 |
— | S1 |
무음모드 켜기 / 끄기 | E,OK |
V<n> |
0~255 | V200 |
진동 세기 | E,OK |
T<n> |
40~300 | T120 |
전방 경고 시작 거리(cm). 중간·근접 단계는 지팡이가 1/2, 1/3로 자동 계산 | E,OK |
G<n> |
5~60 | G20 |
낙하 감지 민감도(cm). 클수록 둔감 | E,OK |
Z |
— | Z |
하방 기준 재보정. 평지에서만 | E,ZOK |
X |
— | X |
SOS 해제 | E,SOSOFF |
P |
— | P |
연결 확인(핑) | E,PONG |
무음모드는 지팡이의 하드웨어 스위치와 OR로 동작합니다. 즉 스위치가
켜져 있으면 앱에서 S0을 보내도 무음이 풀리지 않습니다. 앱은 플래그
비트 2를 보고 실제 상태를 표시해야 하며, 자기가 보낸 명령을 기준으로
UI를 갱신하면 안 됩니다.
[연결 화면] ──연결 성공──> [메인 화면] ──> [설정 화면]
^ │ [기록 화면]
└────연결 끊김 3초────────┘
│
E,SOS ──> [SOS 전체화면]
┌──────────────────────────────┐
│ 스마트 지팡이 │
│ │
│ 페어링된 기기 │
│ ┌────────────────────────┐ │
│ │ HC-06 │ │
│ │ 98:D3:31:F5:2B:9A │ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ Galaxy Buds │ │
│ └────────────────────────┘ │
│ │
│ [ 다시 검색 ] │
│ │
│ 마지막 연결: HC-06 (자동연결) │
└──────────────────────────────┘
- 앱을 켜면 마지막에 연결했던 기기에 자동으로 재연결 시도
- 목록 항목은 기기 이름 + MAC 주소
- 연결 중에는 항목에 진행 표시
┌──────────────────────────────┐
│ ● 연결됨 🔋 78% │
│ │
│ │
│ 87 cm │ ← 화면 높이의 40%. 최대한 크게
│ 전방 │
│ │
├──────────────────────────────┤
│ ⚠ 계단 주의 │ ← FLAG_DROP일 때만 나타남
├──────────────────────────────┤
│ ┌────────┐ ┌────────┐ │
│ │ 소음 │ │ 밝기 │ │
│ │ 42 │ │ 15 어두움│ │
│ └────────┘ └────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 무음 OFF │ │ 기준 보정 │ │
│ └──────────┘ └──────────┘ │
│ │
│ [설정] [기록] │
└──────────────────────────────┘
거리 숫자의 상태별 표시
| 조건 | 숫자 | 배경 | 부가 |
|---|---|---|---|
| 전방 = −1 | — |
기본 | "감지된 것 없음" |
| 경고 없음 | 87 cm |
기본 | — |
FLAG_FRONT 그리고 ≥ 50 cm |
87 cm |
노랑 | — |
FLAG_FRONT 그리고 < 50 cm |
42 cm |
주황 | — |
FLAG_FRONT 그리고 < 30 cm |
24 cm |
빨강 | 폰도 짧게 진동 |
FLAG_DROP |
그대로 | 빨강 배너 추가 | 폰 진동 3회 |
FLAG_UNCAL |
그대로 | — | 상단에 "평지에서 기준 보정을 누르세요" 배너 |
숫자가 떨리지 않게 할 것. 250 ms마다 값이 바뀌면 눈이 아픕니다. 표시값은 최근 3개의 중앙값을 쓰고, 5 cm 미만 변화는 갱신하지 않습니다. 단 경고 플래그 변화는 즉시 반영합니다.
┌──────────────────────────────┐
│ ← 설정 │
│ │
│ 전방 경고 시작 거리 │
│ ├──────●───────┤ 120 cm │ 40 ~ 300
│ 중간 60cm · 근접 40cm ✓ │ ← 자동 계산 결과를 미리 보여줌
│ │
│ 진동 세기 │
│ ├────────●─────┤ 200 ✓ │ 0 ~ 255
│ [ 지금 떨어보기 ] │
│ │
│ 낙하 감지 민감도 │
│ ├───●──────────┤ 15 cm ✓ │ 5 ~ 60
│ 작을수록 예민함 │
│ │
│ ───────────────── │
│ 보호자 전화번호 │
│ [ 010-0000-0000 ] │
│ │
│ ☑ SOS 시 위치 문자 보내기 │
└──────────────────────────────┘
- 슬라이더는 놓는 순간(
onStopTrackingTouch)에만 명령을 보냅니다. 드래그 중에 계속 보내면 HC-06이 밀립니다. - 명령을 보내면
E,OK가 올 때까지 회색 스피너, 오면 초록 체크(✓). 2초 안에 안 오면 빨간 느낌표로 바꾸고 값은 되돌립니다. - 모든 값은
SharedPreferences에 저장하고,E,BOOT를 받으면 다시 전송합니다.
┌──────────────────────────────┐
│ │
│ 🔴 SOS │
│ │
│ 도움 요청이 발생했습니다 │
│ │
│ 14:23:07 │
│ 보호자에게 문자 준비됨 │
│ │
│ ┌────────────────────────┐ │
│ │ 문자 보내기 (SMS 앱) │ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ 보호자에게 전화 │ │
│ └────────────────────────┘ │
│ │
│ [ SOS 해제 ] │ ← 누르면 X 명령 전송
└──────────────────────────────┘
문자 본문:
[스마트지팡이] 도움 요청
시각: 2026-09-02 14:23
위치: https://maps.google.com/?q=37.5665,126.9780
- 자동 발송하지 말고, 내용이 채워진 SMS 앱을 띄우는 방식으로 만듭니다.
(
Intent.ACTION_SENDTO+smsto:+sms_body). 자동 발송은 구글 플레이 심사가 까다롭습니다. - 위치를 아직 못 받았으면
위치: 확인 중으로 먼저 띄우고, 좌표가 오면 본문을 갱신합니다. 위치 때문에 알림이 늦어지면 안 됩니다.
┌──────────────────────────────┐
│ ← 기록 │
│ │
│ 오늘 (9/2) │
│ 전방 경고 47회 │
│ 낙하 경고 3회 │
│ 마지막 낙하 14:23 │
│ │
│ 최근 7일 │
│ ▁▃▅▂▇▄█ │
│ 8/27 9/2 │
│ │
│ [ CSV로 내보내기 ] │
└──────────────────────────────┘
- 경고 횟수는 플래그가 0→1로 바뀐 순간에만 셉니다. 플래그가 켜져 있는 250 ms 패킷마다 세면 안 됩니다.
- 로컬 저장(Room 또는 SharedPreferences+JSON). 서버 없음.
- CSV 내보내기는 발명 보고서에 붙일 데이터가 되므로 꼭 넣습니다.
| 상태 | 진입 조건 | 이탈 조건 | UI |
|---|---|---|---|
DISCONNECTED |
앱 시작, 소켓 종료 | 사용자가 기기 선택 | 연결 화면 |
CONNECTING |
소켓 연결 시도 | 성공 / 실패 | 스피너 |
CONNECTED |
소켓 열림 | 3초간 D, 없음 / 예외 |
메인 화면 |
STALE |
3초간 패킷 없음 | 패킷 재수신 / 5초 경과 | "신호 없음" 흐린 오버레이 |
RECONNECTING |
STALE 5초 경과 |
성공 / 3회 실패 | 자동 재연결, 3회 실패 후 연결 화면 |
| 상황 | 잘못된 동작 | 올바른 동작 |
|---|---|---|
깨진 줄 수신 (D,87,3) |
파싱 예외로 앱 종료 | 그 줄만 버리고 계속 |
| 숫자가 아닌 필드 | NumberFormatException |
그 줄만 버림 |
| 필드 개수 불일치 | 배열 인덱스 초과 | 그 줄만 버림 |
E,SOS 3초마다 반복 |
문자를 계속 띄움 | 최초 1회만. SOS 해제 전까지 잠금 |
| 화면 꺼짐 | 소켓 끊김 | 포그라운드 서비스로 유지 |
| 블루투스 꺼짐 | 무한 재연결 시도 | 사용자에게 켜달라고 안내 후 중단 |
| 안드로이드 12+ 권한 미허용 | SecurityException |
런타임 권한 요청 |
| 위치 권한 거부 | SOS 문자 실패 | 좌표 없이 시각만이라도 발송 |
| 수신 스레드에서 UI 갱신 | 크래시 | 메인 스레드로 넘김 |
BLUETOOTH_CONNECT (API 31+, 런타임 요청)
BLUETOOTH_SCAN (API 31+, 런타임 요청)
BLUETOOTH / BLUETOOTH_ADMIN (API 30 이하)
ACCESS_FINE_LOCATION (SOS 위치)
FOREGROUND_SERVICE
POST_NOTIFICATIONS (API 33+)
- 거리 숫자는 48 sp 이상, 화면 밝기와 무관하게 대비 7:1 이상
- 모든 버튼에
contentDescription - 색만으로 상태를 구분하지 말 것 — 색 + 텍스트 + 폰 진동을 함께
- TalkBack으로 메인 화면을 훑었을 때 "전방 87센티미터, 계단 주의 없음, 배터리 78퍼센트" 순으로 읽히도록 순서 지정
- 거리 값이 경고 구간에 들어갈 때만 TalkBack
announceForAccessibility호출 (250 ms마다 읽으면 못 씀)
각 블록을 순서대로 AI에게 넣습니다. 1단계에는 위 §1 전체를 함께 붙여넣으세요.
안드로이드 앱을 코틀린으로 만들어 줘. 최소 API 24, 대상 API 34.
Jetpack Compose를 쓰고, 화면은 하나만 먼저 만들자.
목적: HC-06 블루투스 모듈(SPP)에 연결해서 텍스트 줄을 주고받는 앱.
아래 통신 규격을 지켜야 해.
[여기에 §1 통신 규격 전체를 붙여넣기]
이번 단계에서 만들 것:
1. 페어링된 블루투스 기기 목록 화면. 탭하면 연결.
2. 연결/해제 로직. SPP UUID 사용. 연결은 별도 코루틴에서.
3. 수신 스레드: InputStream을 줄 단위로 읽어 Flow로 흘려보내기.
\r 은 버리고 \n 으로 자를 것.
4. 송신 함수: 문자열 끝에 \n 을 붙여 전송.
5. 받은 줄을 화면 아래에 로그로 그대로 표시 (디버그용).
6. 안드로이드 12 이상의 BLUETOOTH_CONNECT / BLUETOOTH_SCAN 런타임
권한 요청을 반드시 포함.
파일 목록과 전체 구조를 먼저 보여주고, 그다음 코드를 작성해 줘.
좋아. 이제 받은 줄을 해석하는 부분을 만들자.
1. CaneState 데이터 클래스: frontCm, dropGap, noise, light, battery, flags
그리고 flags에서 파생되는 val: isFront, isDrop, isMute, isSos, isUncal
2. parseLine(line: String): ParsedResult
- "D,"로 시작하면 상태 패킷. 필드 7개가 정확히 맞고 전부 정수일 때만 성공.
- "E,"로 시작하면 이벤트. 이름을 그대로 반환.
- 그 외 또는 파싱 실패는 null 반환하고 조용히 무시.
3. 파서에 대한 단위 테스트를 반드시 작성해 줘. 아래 케이스를 포함:
"D,87,3,42,15,78,0" 정상
"D,-1,999,38,12,76,18" 음수와 999
"D,87,3" 필드 부족 → null
"D,87,3,42,15,78,abc" 숫자 아님 → null
"E,SOS" 이벤트
"" 빈 줄 → null
"쓰레기" → null
4. ViewModel에 최신 CaneState를 StateFlow로 보관.
5. 3초간 D 패킷이 없으면 연결 상태를 STALE로 바꾸는 타이머.
파서가 어떤 입력에도 예외를 던지지 않아야 해. 이게 이 단계의 핵심이야.
메인 화면을 만들자. 아래 레이아웃과 규칙을 그대로 따라 줘.
[여기에 §2.3 메인 화면 전체를 붙여넣기]
추가 요구사항:
- 표시할 거리값은 최근 3개의 중앙값을 쓰고, 5cm 미만 변화면 갱신하지 않아.
단 경고 플래그가 바뀌면 즉시 반영해.
- FLAG_DROP이 0에서 1로 바뀌는 순간에만 폰을 3회 진동시켜.
플래그가 켜져 있는 동안 매 패킷마다 진동시키면 안 돼.
- 무음 버튼을 누르면 S1/S0을 보내되, 버튼의 표시 상태는
다음 패킷의 FLAG_MUTE 비트를 보고 갱신해. 내가 보낸 명령 기준으로
바꾸면 안 돼. (지팡이에 하드웨어 스위치가 따로 있어서 OR로 동작해)
- 기준 보정 버튼은 Z를 보내고 E,ZOK를 기다려. 누른 직후 2초간 비활성화.
설정 화면을 만들자.
[여기에 §2.4 설정 화면 전체를 붙여넣기]
- 슬라이더는 onValueChangeFinished에서만 명령을 전송.
- 명령 전송 후 2초 안에 E,OK가 안 오면 실패로 처리하고 값을 되돌려.
- 모든 설정값을 DataStore에 저장하고, E,BOOT를 받으면 저장된 값을
T, V, G 순서로 200ms 간격을 두고 다시 전송해. (한꺼번에 보내면 밀려)
- "지금 떨어보기" 버튼은 V<현재값>을 보내고 1초 뒤 원래 값으로 되돌려.
SOS 기능을 만들자. 이게 이 앱에서 제일 중요한 기능이야.
[여기에 §2.5 SOS 화면 전체를 붙여넣기]
동작 순서:
1. E,SOS 수신 → sosLatch가 false일 때만 아래를 실행하고 latch를 true로.
2. 즉시 SOS 전체화면을 띄우고 알림음/진동.
3. 동시에 FusedLocationProvider로 현재 위치를 요청 (최대 10초 대기).
4. 좌표가 오면 문자 본문을 갱신. 안 오면 "위치: 확인 불가"로 진행.
5. "문자 보내기" 버튼 → Intent.ACTION_SENDTO, smsto:<보호자번호>,
sms_body에 본문. 자동 발송 아님.
6. "SOS 해제" 버튼 → X 전송. E,SOSOFF를 받으면 latch를 false로 되돌리고
화면을 닫아.
주의: E,SOS는 해제 전까지 3초마다 계속 들어와. latch가 없으면
문자 앱이 3초마다 뜨는 참사가 나. latch를 반드시 넣어 줘.
기록 화면을 만들자.
[여기에 §2.6 기록 화면 전체를 붙여넣기]
- Room으로 WarnEvent 테이블: id, type(FRONT/DROP/SOS), timestamp
- 플래그가 0에서 1로 바뀌는 순간에만 한 건 기록.
- 오늘 집계와 최근 7일 막대그래프.
- CSV 내보내기: 날짜,종류,시각 형식으로 파일 생성 후 공유 인텐트.
마지막으로 실사용에 필요한 것들을 붙이자.
1. 블루투스 연결과 수신을 포그라운드 서비스로 옮겨서 화면이 꺼져도
유지되게 해. 알림에는 현재 연결 상태와 배터리%를 표시.
2. 아래 접근성 요구사항을 전부 반영해:
[여기에 §3.4 접근성 전체를 붙여넣기]
3. 아래 예외 상황을 전부 처리해:
[여기에 §3.2 표 전체를 붙여넣기]
4. 앱 아이콘과 이름을 "스마트 지팡이"로 설정.
앱 제작 전에 지팡이 쪽이 제대로 도는지부터 확인하세요.
플레이스토어의 Serial Bluetooth Terminal 앱으로 HC-06에 연결합니다.
| 보이는 것 | 뜻 |
|---|---|
D, 로 시작하는 줄이 초당 4개씩 흐름 |
정상. 앱을 만들어도 됩니다 |
| 아무것도 안 옴 | HC-06 배선 확인. TXD가 나노 D7에 갔는지 |
| 글자가 깨져서 나옴 | 통신 속도 불일치. HC-06을 9600으로 맞출 것 |
D,-1,999,... 만 계속 나옴 |
초음파 센서 배선 문제 |
| 연결 자체가 안 됨 | 페어링 PIN (1234 또는 0000) |
터미널에서 S1, V150, Z 를 직접 쳐 보면 명령 쪽도 검증됩니다.
여기까지 되면 앱은 화면을 붙이는 일만 남습니다.
| 증상 | 십중팔구 원인 |
|---|---|
연결 시 SecurityException |
안드로이드 12+ BLUETOOTH_CONNECT 런타임 권한 누락 |
| 페어링된 기기 목록이 비어 있음 | 같은 권한 문제. 또는 bondedDevices 대신 스캔을 씀 |
| 연결은 되는데 데이터가 안 옴 | InputStream을 바이트 단위로 안 읽고 readLine()을 썼을 때 자주 발생 |
| 몇 초 뒤 앱이 죽음 | 수신 스레드에서 직접 UI를 건드림 |
| 문자 앱이 계속 뜸 | SOS latch 누락 (5단계 참조) |
| 화면 끄면 끊김 | 포그라운드 서비스 미적용 (7단계) |
이럴 때는 증상만 말하지 말고 위 표의 원인을 지목해서 다시 요청하세요. "안 돼요"보다 "BLUETOOTH_CONNECT 런타임 권한 요청 코드를 넣어줘"가 훨씬 빨리 고쳐집니다.
블록 코딩으로 가겠다면 1~7단계 대신 아래 구성을 쓰세요.
| 컴포넌트 | 역할 |
|---|---|
BluetoothClient1 |
연결. ConnectionTimeout 5000 |
ListPicker1 |
AddressesAndNames로 페어링 목록 |
Clock1 (200 ms) |
BytesAvailableToReceive > 0 이면 ReceiveText(-1) |
Clock2 (1000 ms) |
3초간 수신 없으면 "연결 끊김" 표시 |
Texting1 |
SOS 문자 |
LocationSensor1 |
SOS 좌표 |
TinyDB1 |
설정값 저장 |
주의: 앱인벤터의 ReceiveText(-1)은 줄 단위가 아니라 버퍼 통째로
읽습니다. 받은 문자열을 전역 버퍼에 이어 붙이고 \n으로 split해서
마지막 조각은 버퍼에 남겨두는 처리가 반드시 필요합니다. 이것만
빠뜨리지 않으면 나머지는 어렵지 않습니다.