Skip to content

Latest commit

 

History

History
514 lines (408 loc) · 22.3 KB

app-spec.md

File metadata and controls

514 lines (408 loc) · 22.3 KB

스마트 지팡이 앱 — AI 제작 명세서 v2

안드로이드 전용. HC-06(SPP) 연결. 이 문서 하나로 앱이 완성되도록 썼습니다.

쓰는 법: 아래 「7단계 프롬프트」를 순서대로 하나씩 AI에게 넣습니다. 한 번에 다 넣으면 AI가 절반쯤에서 대충 넘어갑니다. 단계마다 코드를 받아 빌드해 보고 다음으로 갑니다. 각 단계 프롬프트는 회색 블록 안의 내용을 그대로 복사하면 됩니다.

먼저 §1 통신 규격과 §2 화면 설계를 읽고, 1단계 프롬프트에 §1을 통째로 붙여넣으세요. AI는 이 규격을 계속 참조해야 합니다.


§1 통신 규격

1.1 연결

항목 값
방식 고전 블루투스 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은 버릴 것)

1.2 지팡이 → 앱 : 상태 패킷

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 하방 기준 미보정 "평지에서 보정하세요" 안내 배너

1.3 지팡이 → 앱 : 이벤트

상태 패킷 사이에 끼어들어 옵니다. 형식은 E,<이름>.

메시지 언제 앱이 할 일
E,BOOT 지팡이 전원이 켜졌을 때 저장해 둔 설정값을 다시 전송
E,SOS SOS 발동. 해제 전까지 3초마다 반복 최초 1회만 보호자 알림. 이후는 무시
E,PONG P 명령에 대한 응답 연결 살아있음 확인
E,OK 설정 변경 성공 슬라이더 옆에 체크 표시
E,ZOK 하방 기준 보정 완료 "보정 완료" 토스트
E,SOSOFF SOS 해제됨 경고 화면 닫기
E,ERR 알 수 없는 명령 로그만 남김

1.4 앱 → 지팡이 : 명령

각 명령 끝에 \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를 갱신하면 안 됩니다.


§2 화면 설계

2.1 화면 흐름

[연결 화면] ──연결 성공──> [메인 화면] ──> [설정 화면]
     ^                        │              [기록 화면]
     └────연결 끊김 3초────────┘
                              │
                    E,SOS ──> [SOS 전체화면]

2.2 연결 화면

┌──────────────────────────────┐
│  스마트 지팡이                │
│                              │
│  페어링된 기기                │
│  ┌────────────────────────┐  │
│  │ HC-06                  │  │
│  │ 98:D3:31:F5:2B:9A      │  │
│  └────────────────────────┘  │
│  ┌────────────────────────┐  │
│  │ Galaxy Buds            │  │
│  └────────────────────────┘  │
│                              │
│  [ 다시 검색 ]                │
│                              │
│  마지막 연결: HC-06 (자동연결) │
└──────────────────────────────┘
  • 앱을 켜면 마지막에 연결했던 기기에 자동으로 재연결 시도
  • 목록 항목은 기기 이름 + MAC 주소
  • 연결 중에는 항목에 진행 표시

2.3 메인 화면 (핵심)

┌──────────────────────────────┐
│  ● 연결됨          🔋 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 미만 변화는 갱신하지 않습니다. 단 경고 플래그 변화는 즉시 반영합니다.

2.4 설정 화면

┌──────────────────────────────┐
│  ← 설정                       │
│                              │
│  전방 경고 시작 거리           │
│  ├──────●───────┤  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를 받으면 다시 전송합니다.

2.5 SOS 화면

┌──────────────────────────────┐
│                              │
│         🔴 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). 자동 발송은 구글 플레이 심사가 까다롭습니다.
  • 위치를 아직 못 받았으면 위치: 확인 중 으로 먼저 띄우고, 좌표가 오면 본문을 갱신합니다. 위치 때문에 알림이 늦어지면 안 됩니다.

2.6 기록 화면

┌──────────────────────────────┐
│  ← 기록                       │
│                              │
│  오늘 (9/2)                   │
│  전방 경고     47회            │
│  낙하 경고      3회            │
│  마지막 낙하   14:23           │
│                              │
│  최근 7일                     │
│   ▁▃▅▂▇▄█                   │
│  8/27          9/2           │
│                              │
│  [ CSV로 내보내기 ]            │
└──────────────────────────────┘
  • 경고 횟수는 플래그가 0→1로 바뀐 순간에만 셉니다. 플래그가 켜져 있는 250 ms 패킷마다 세면 안 됩니다.
  • 로컬 저장(Room 또는 SharedPreferences+JSON). 서버 없음.
  • CSV 내보내기는 발명 보고서에 붙일 데이터가 되므로 꼭 넣습니다.

§3 상태 전이와 예외 처리

3.1 연결 상태

상태 진입 조건 이탈 조건 UI
DISCONNECTED 앱 시작, 소켓 종료 사용자가 기기 선택 연결 화면
CONNECTING 소켓 연결 시도 성공 / 실패 스피너
CONNECTED 소켓 열림 3초간 D, 없음 / 예외 메인 화면
STALE 3초간 패킷 없음 패킷 재수신 / 5초 경과 "신호 없음" 흐린 오버레이
RECONNECTING STALE 5초 경과 성공 / 3회 실패 자동 재연결, 3회 실패 후 연결 화면

3.2 반드시 막아야 하는 것

상황 잘못된 동작 올바른 동작
깨진 줄 수신 (D,87,3) 파싱 예외로 앱 종료 그 줄만 버리고 계속
숫자가 아닌 필드 NumberFormatException 그 줄만 버림
필드 개수 불일치 배열 인덱스 초과 그 줄만 버림
E,SOS 3초마다 반복 문자를 계속 띄움 최초 1회만. SOS 해제 전까지 잠금
화면 꺼짐 소켓 끊김 포그라운드 서비스로 유지
블루투스 꺼짐 무한 재연결 시도 사용자에게 켜달라고 안내 후 중단
안드로이드 12+ 권한 미허용 SecurityException 런타임 권한 요청
위치 권한 거부 SOS 문자 실패 좌표 없이 시각만이라도 발송
수신 스레드에서 UI 갱신 크래시 메인 스레드로 넘김

3.3 필요 권한

BLUETOOTH_CONNECT   (API 31+, 런타임 요청)
BLUETOOTH_SCAN      (API 31+, 런타임 요청)
BLUETOOTH / BLUETOOTH_ADMIN  (API 30 이하)
ACCESS_FINE_LOCATION         (SOS 위치)
FOREGROUND_SERVICE
POST_NOTIFICATIONS  (API 33+)

3.4 접근성 — 이 앱은 시각장애인 본인도 쓴다

  • 거리 숫자는 48 sp 이상, 화면 밝기와 무관하게 대비 7:1 이상
  • 모든 버튼에 contentDescription
  • 색만으로 상태를 구분하지 말 것 — 색 + 텍스트 + 폰 진동을 함께
  • TalkBack으로 메인 화면을 훑었을 때 "전방 87센티미터, 계단 주의 없음, 배터리 78퍼센트" 순으로 읽히도록 순서 지정
  • 거리 값이 경고 구간에 들어갈 때만 TalkBack announceForAccessibility 호출 (250 ms마다 읽으면 못 씀)

§4 7단계 프롬프트

각 블록을 순서대로 AI에게 넣습니다. 1단계에는 위 §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 런타임
   권한 요청을 반드시 포함.

파일 목록과 전체 구조를 먼저 보여주고, 그다음 코드를 작성해 줘.

2단계 — 파서와 데이터 모델

좋아. 이제 받은 줄을 해석하는 부분을 만들자.

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로 바꾸는 타이머.

파서가 어떤 입력에도 예외를 던지지 않아야 해. 이게 이 단계의 핵심이야.

3단계 — 메인 화면

메인 화면을 만들자. 아래 레이아웃과 규칙을 그대로 따라 줘.

[여기에 §2.3 메인 화면 전체를 붙여넣기]

추가 요구사항:
- 표시할 거리값은 최근 3개의 중앙값을 쓰고, 5cm 미만 변화면 갱신하지 않아.
  단 경고 플래그가 바뀌면 즉시 반영해.
- FLAG_DROP이 0에서 1로 바뀌는 순간에만 폰을 3회 진동시켜.
  플래그가 켜져 있는 동안 매 패킷마다 진동시키면 안 돼.
- 무음 버튼을 누르면 S1/S0을 보내되, 버튼의 표시 상태는
  다음 패킷의 FLAG_MUTE 비트를 보고 갱신해. 내가 보낸 명령 기준으로
  바꾸면 안 돼. (지팡이에 하드웨어 스위치가 따로 있어서 OR로 동작해)
- 기준 보정 버튼은 Z를 보내고 E,ZOK를 기다려. 누른 직후 2초간 비활성화.

4단계 — 설정 화면

설정 화면을 만들자.

[여기에 §2.4 설정 화면 전체를 붙여넣기]

- 슬라이더는 onValueChangeFinished에서만 명령을 전송.
- 명령 전송 후 2초 안에 E,OK가 안 오면 실패로 처리하고 값을 되돌려.
- 모든 설정값을 DataStore에 저장하고, E,BOOT를 받으면 저장된 값을
  T, V, G 순서로 200ms 간격을 두고 다시 전송해. (한꺼번에 보내면 밀려)
- "지금 떨어보기" 버튼은 V<현재값>을 보내고 1초 뒤 원래 값으로 되돌려.

5단계 — SOS와 보호자 알림

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를 반드시 넣어 줘.

6단계 — 기록 화면

기록 화면을 만들자.

[여기에 §2.6 기록 화면 전체를 붙여넣기]

- Room으로 WarnEvent 테이블: id, type(FRONT/DROP/SOS), timestamp
- 플래그가 0에서 1로 바뀌는 순간에만 한 건 기록.
- 오늘 집계와 최근 7일 막대그래프.
- CSV 내보내기: 날짜,종류,시각 형식으로 파일 생성 후 공유 인텐트.

7단계 — 포그라운드 서비스와 접근성

마지막으로 실사용에 필요한 것들을 붙이자.

1. 블루투스 연결과 수신을 포그라운드 서비스로 옮겨서 화면이 꺼져도
   유지되게 해. 알림에는 현재 연결 상태와 배터리%를 표시.
2. 아래 접근성 요구사항을 전부 반영해:

[여기에 §3.4 접근성 전체를 붙여넣기]

3. 아래 예외 상황을 전부 처리해:

[여기에 §3.2 표 전체를 붙여넣기]

4. 앱 아이콘과 이름을 "스마트 지팡이"로 설정.

§5 앱 없이 먼저 확인하기

앱 제작 전에 지팡이 쪽이 제대로 도는지부터 확인하세요. 플레이스토어의 Serial Bluetooth Terminal 앱으로 HC-06에 연결합니다.

보이는 것 뜻
D, 로 시작하는 줄이 초당 4개씩 흐름 정상. 앱을 만들어도 됩니다
아무것도 안 옴 HC-06 배선 확인. TXD가 나노 D7에 갔는지
글자가 깨져서 나옴 통신 속도 불일치. HC-06을 9600으로 맞출 것
D,-1,999,... 만 계속 나옴 초음파 센서 배선 문제
연결 자체가 안 됨 페어링 PIN (1234 또는 0000)

터미널에서 S1, V150, Z 를 직접 쳐 보면 명령 쪽도 검증됩니다. 여기까지 되면 앱은 화면을 붙이는 일만 남습니다.


§6 AI가 만든 코드가 안 돌 때

증상 십중팔구 원인
연결 시 SecurityException 안드로이드 12+ BLUETOOTH_CONNECT 런타임 권한 누락
페어링된 기기 목록이 비어 있음 같은 권한 문제. 또는 bondedDevices 대신 스캔을 씀
연결은 되는데 데이터가 안 옴 InputStream을 바이트 단위로 안 읽고 readLine()을 썼을 때 자주 발생
몇 초 뒤 앱이 죽음 수신 스레드에서 직접 UI를 건드림
문자 앱이 계속 뜸 SOS latch 누락 (5단계 참조)
화면 끄면 끊김 포그라운드 서비스 미적용 (7단계)

이럴 때는 증상만 말하지 말고 위 표의 원인을 지목해서 다시 요청하세요. "안 돼요"보다 "BLUETOOTH_CONNECT 런타임 권한 요청 코드를 넣어줘"가 훨씬 빨리 고쳐집니다.


부록 — MIT 앱인벤터로 만들 경우

블록 코딩으로 가겠다면 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해서 마지막 조각은 버퍼에 남겨두는 처리가 반드시 필요합니다. 이것만 빠뜨리지 않으면 나머지는 어렵지 않습니다.