IoTAgent ↔ AWS IoT Core 토픽 명세
Sample 설계 참고 자료: 아래 토픽·메시지·운영 제안은 제품 확정 규격으로 강제하지 않는다. APP·Backend·IoTAgent 연결은 SoC와 APP·Backend 담당이 협의한다. 직접 맞출 기기 경계는 담당별 구현 범위와 두 구현 요청서를 따른다.
제품 연동 보강: 서버 저장 ACK와 완료 판정, 재전송·재부팅·시계 오차, 삭제 중 오프라인·늦은 commit, 인증서·기기 권한, 왕복 시험표. 제안 규칙의 삭제·동의 통지용 신규 토픽·필드는 아직 제품 계약 확정 전이며 이 문서가 가상의 wire 이름을 정의하지 않는다.
09-17 관심사 변경: App 저장은 서버 HTTPS로 처리한다. AI-Agent의 getVoicePersonalization은 IoTAgent가 request/voicePersonalization/{requestId}로 발행하고, 서버 결과는 기존 command/voiceProfileEnrollment의 action=personalizationResult로 수신한다. 전체 신규 규격·예제·그림. 현재 송신 구현은 샘플 MQTT이며 AWS 제품 연결은 별도다. 아래 interestsSet/interestsGet과 그 성공 예제는 중단된 로컬 규격이다.
화자 등록·조회·삭제와 화자별 관심사을 주고받는 MQTT 규격이다. 기존 코드의 수신 형식은 유지하고, 아직 연결되지 않은 제품 송신·저장 완료 경로는 별도로 표시한다.
1. 누가 보내고 받는가
| 주체 | 보내는 것 | 받는 것 |
|---|---|---|
| 모바일 App | Backend에 HTTPS 등록·관리 요청 | Backend의 상태·결과 |
| AWS Backend | IoT Core에 기기 명령 발행(PUBLISH) | 기기 응답을 MQTT 구독(SUBSCRIBE) 또는 IoT Rule로 수신 |
| AWS IoT Core | 발행된 메시지를 구독자·Rule에 전달 | Backend와 IoTAgent의 MQTT 메시지 |
| SoC IoTAgent | 기기 상태·관리 결과 발행 | 자기 기기의 명령 토픽 구독 |
| DeviceAgent | Binder 요청·결과 중계 | IoTAgent, 등록 App, AI-Agent의 Bundle |
| 등록 App·AI-Agent | DeviceAgent에 상태·결과 callback | 화면 실행·등록 제어·관리 요청 |
IoT Core는 등록 결과를 판단하는 주체가 아니다. PUBLISH는 보내기, SUBSCRIBE는 해당 주소의 메시지를 받도록 등록하기다. 모바일과 등록 App은 MQTT에 직접 연결하지 않는다.
2. 구현 상태
| 경로 | 현재 코드 | 제품 작업 |
|---|---|---|
| AWS 명령 → IoTAgent | CmdTopic에 voiceProfileEnrollment 분기 있음 |
실제 AWS 인증·토픽 권한·명령 수신 통합 시험 |
| 로컬 명령 → IoTAgent | 시험 Bridge가 같은 명령 처리 함수 호출 | 제품에서 시험 Bridge를 사용하지 않음 |
| DeviceAgent callback → IoTAgent | 관리 결과·등록 상태 수신 | 결과 종류별 JSON 변환 확인 |
| IoTAgent → MQTT 결과 | VoiceProfileMqttTestBridge.publishResult()만 연결 |
기존 AWS 연결에 제품 publish 연결 |
| 임베딩 → Cloud 저장 → ACK | DeviceAgent 대기 저장까지만 있음 | 업로드·저장 ACK·재전송 별도 구현 |
현재 소스는 AWS 전체 연동 완료를 뜻하지 않는다. 이 문서 작업에서 앱 코드·기기 설정은 변경하지 않았다.
3. 토픽 목록
{deviceId}는 IoTAgent의 serialId와 동일한 기기 식별자다. {apiVersion}은 apiVersion/mUtil.getVersion()에서 오는 토픽 버전이며 아래 예제는 v1이다. JSON의 contractVersion과는 별개다.
| 구분 | 실제 토픽 | 발행자 | 수신자 | QoS / Retain |
|---|---|---|---|---|
| 명령 | skmg/airbot/{deviceId}/v1/command/voiceProfileEnrollment |
AWS Backend | 해당 기기 IoTAgent | 제품 권장 1 / false |
| 상태·관리 결과 | skmg/airbot/{deviceId}/v1/response/voiceProfileEnrollment/{requestId} |
IoTAgent | AWS Backend 또는 IoT Rule | 로컬 구현 1 / false, 제품도 동일 권장 |
| 임베딩 업로드·Cloud 저장 ACK | 현행 토픽 없음 | 제품 계약 확정 필요 | 제품 계약 확정 필요 | 미정 |
Backend의 기기별 응답 구독 필터는 skmg/airbot/{deviceId}/v1/response/voiceProfileEnrollment/+다. +는 요청 ID 한 구간을 받기 위한 구독 필터이며 발행 주소에 넣지 않는다.
등록·삭제 명령은 action, 결과는 method로 구분한다. 관심사는 위 신규 조회 토픽을 사용한다. 기존 그림의 별도 event 토픽이나 syncAck는 현행 구현 명령이 아니다.
제품 발신 규칙: requestId는 UUID처럼 영문·숫자·하이픈·밑줄로 구성하고 /, +, #를 금지한다. 현재 IoTAgent 검사는 비어 있음/128자 초과만 확인하므로 토픽 안전 문자 검증은 추가 보완이 필요하다.
4. 명령 공통 형식
{
"action": "start",
"payload": {
"contractVersion": "voice-profile-v2",
"method": "startVoiceProfileEnrollment",
"requestId": "vp-example-001",
"nickname": "Niky",
"locale": "ko-KR",
"personalizationEnabled": true,
"issuedAtMs": 0,
"expiresAtMs": 0
}
}
위 시각 0은 설명용이다. 송신할 때 현재 Unix 밀리초와 미래 만료 시각으로 바꾼다. 0 그대로는 만료 거절된다. 이 메시지가 수신되어도 바로 녹음하지 않고 등록 App을 연 뒤 사용자가 시작한다.
| 필드 | JSON 타입 | 의미·발신 규칙 | 현재 수신 동작 |
|---|---|---|---|
action |
string | 다음 표의 작업 종류 | 누락 시 start |
payload |
object | 명령 본문 | 객체가 없으면 최상위 메시지를 본문으로 읽음 |
contractVersion |
string | 등록/프로필은 voice-profile-v2, 서버 관심사는 voice-personalization-v1 | 메서드에 맞는 버전 검사 |
method |
string | action에 맞는 Binder 메서드 | 서버 관심사 계열은 일치 검사; 프로필 계열은 action 또는 method 분기이므로 불일치 입력 금지 |
requestId |
string | 명령과 결과를 연결하는 ID, 1~128자 | 결과 토픽의 마지막 구간으로도 사용 |
issuedAtMs |
integer(int64) | 명령 생성 시각 | start 누락 시 현재 시각; 나머지 명령은 Bundle 생성 시 현재 시각으로 대체 |
expiresAtMs |
integer(int64) | 이 시각 이후 명령 실행 금지 | 현재 시각 이하 거절; 누락 시 Long.MAX_VALUE |
기기 주소는 토픽으로 선택한다. payload의 deviceId만으로 다른 기기를 선택하지 않는다. 제품 Backend는 발행 전에 계정의 기기 접근 권한을 확인해야 한다.
5. 작업별 입력과 결과
모든 작업은 4절 공통 필드를 보낸다. 아래 표는 추가 입력이다. 버전 P는 voice-profile-v2, M은 voice-memory-v1이다.
| action | 버전 | method | 추가 입력 | 결과 method |
|---|---|---|---|---|
start |
P | startVoiceProfileEnrollment |
nickname, locale, personalizationEnabled | voiceProfileStateChanged 여러 번 |
cancel |
P | cancelVoiceProfileEnrollment |
reason 선택; 취소할 등록과 같은 requestId | voiceProfileStateChanged |
list |
P | listVoiceProfiles |
없음 | voiceProfileListResult |
delete |
P | deleteVoiceProfile |
voiceId | voiceProfileDeleteResult |
interestsSet |
M | setVoiceProfileInterests |
voiceId, interestCodes | voiceProfileInterestsResult |
interestsGet |
M | getVoiceProfileInterests |
voiceId | voiceProfileInterestsResult |
| 추가 필드 | JSON 타입 | 의미·범위 |
|---|---|---|
nickname |
string | 표시 이름. trim 후 1~40자; voiceId가 아님 |
locale |
string | 한·영 연동 송신값: 한국어 ko-KR, 영어 en-US. IoTAgent 누락 기본값은 ko-KR; 현재 검사에는 언어 목록 제한 없음. 경로별 기본값·검증 |
personalizationEnabled |
boolean | 초기 개인화 값. 기본 true; 등록 App의 최종 선택은 별도 Binder 요청 |
reason |
string | 취소 이유. 기본 MOBILE_CANCELLED |
voiceId |
string | 기기에 등록된 화자 키. 1~128자, 영문·숫자·밑줄·하이픈 |
interestCodes |
string[] | 저장할 관심사 코드 전체 목록. 최대 16개; 빈 배열은 비우기 |
발음 보조 값은 MQTT 필드가 아니다.
관심사 코드: 실제 허용 목록
송신값은 아래 대문자 코드다. 표시 이름을 대신 보내지 않는다. 목록은 AI-Agent VoiceMemoryStore.ALLOWED_INTEREST_CODES 기준이다.
| 코드 | 표시 의미 |
|---|---|
WEATHER |
날씨 |
AIR_QUALITY |
공기질 |
HEALTH |
건강 |
SCHEDULE |
일정 |
TRAVEL |
여행 |
FOOD |
음식 |
CULTURE |
문화 |
SPORTS |
스포츠 |
FINANCE |
금융 |
TECHNOLOGY |
기술 |
interestsSet은 추가 명령이 아니라 전체 목록 교체다. []는 모두 해제한다. 송신 측은 중복 없는 배열을 보낸다. 최대 16개는 배열 상한이고 현재 정의된 서로 다른 코드는 10개다. 허용 목록 밖 코드의 Service 검증 거절은 현재 무응답이 될 수 있으며, 전용 MQTT 오류 코드가 구현되어 있지는 않다.
조회·삭제·관심사 예제
{
"action": "list",
"payload": {
"contractVersion": "voice-profile-v2",
"method": "listVoiceProfiles",
"requestId": "list-001",
"issuedAtMs": 0,
"expiresAtMs": 0
}
}
{
"action": "delete",
"payload": {
"contractVersion": "voice-profile-v2",
"method": "deleteVoiceProfile",
"requestId": "delete-001",
"voiceId": "vp-example-001",
"issuedAtMs": 0,
"expiresAtMs": 0
}
}
{
"action": "interestsSet",
"payload": {
"contractVersion": "voice-memory-v1",
"method": "setVoiceProfileInterests",
"requestId": "interests-001",
"voiceId": "vp-example-001",
"interestCodes": [
"WEATHER"
],
"issuedAtMs": 0,
"expiresAtMs": 0
}
}
관리 요청마다 새 requestId를 사용한다. 취소만 대상 등록의 requestId를 재사용한다. 기록/관심사/삭제의 voiceId는 반드시 기기 목록에서 받은 실제 값으로 지정한다.
6. 응답 공통 형식
전송 확인과 업무 결과의 구분
| 확인 단계 | 받는 값 | 의미 | 보장하지 않는 것 |
|---|---|---|---|
| Backend → IoT Core | QoS 1의 MQTT PUBACK | Broker가 발행 메시지를 확인 | 기기 수신·화면 실행·등록 완료 |
| IoTAgent → DeviceAgent | Binder sendModuleCommand():Int |
내부 호출의 수락/거절. 0은 해당 경로 처리, 음수는 오류 | 이 Int가 MQTT 응답 본문으로 자동 전달되지 않음 |
| AI-Agent → IoTAgent → Backend | 아래 결과 JSON | 상태 또는 관리 작업 결과 | Cloud 임베딩 저장 완료는 별도 ACK 필요 |
MQTT에는 HTTP의 200/400 같은 업무 상태 코드가 없다. success, state, reasonCode는 MQTT가 아니라 이 애플리케이션 계약의 필드다. 현재 내부 Binder 오류는 로그만 남기고 MQTT 결과 없이 끝날 수 있다.
응답은 action/payload로 감싸지 않는 최상위 JSON이다. requestId는 토픽 마지막 구간과 본문이 같아야 한다.
{
"contractVersion": "voice-profile-v2",
"method": "voiceProfileListResult",
"requestId": "list-001",
"success": true,
"reasonCode": "",
"profiles": [
{
"voiceId": "vp-example-001",
"nickname": "Niky"
}
]
}
| 필드 | 타입 | 설명 |
|---|---|---|
contractVersion |
string | 결과의 계약 버전 |
method |
string | 결과 종류; 아래 표로 분기 |
requestId |
string | 원래 관리/등록 요청 ID |
success |
boolean | 관리 결과 성공 여부. 등록 상태 이벤트에는 이 값으로 성공 판정하지 않음 |
reasonCode |
string | 거절/실패 이유. 성공 또는 이유 없음은 빈 문자열 가능 |
현재 Bridge는 success가 없는 Bundle에도 false를 삽입한다. 등록 상태 Bundle에는 원래 success가 없으므로 COMPLETED라도 MQTT JSON의 success=false일 수 있다. Backend는 method가 상태 이벤트면 state/terminal/voiceId로 판정해야 한다.
| 결과 method | 추가 필드·타입 | 뜻 |
|---|---|---|
voiceProfileDeleteResult |
voiceId: string | 삭제 대상. 모든 Cloud/대기 사본 삭제 보장은 아님 |
voiceProfileInterestsResult |
voiceId: string, interestCodes: string[] | 화자별 관심사 현재 값 |
voiceProfileStateChanged |
아래 상태 표 | 등록 상태 변화 |
실패 시 추가 성공 필드는 없을 수 있다. method/requestId와 success 또는 state를 먼저 확인한다. Binder 필드별 원본은 전체 함수·Bundle 명세를 기준으로 한다.
요청별 완료 판정과 현재 오류 값
아래 reasonCode는 현재 Service 코드의 관리 결과 값이다. 네트워크에서 항상 전달된다는 보장이 아니며, 수신 검증에서 거절되면 결과 자체가 없을 수 있다.
| action | 완료/성공 판정 | 실패 reasonCode 또는 예외 |
|---|---|---|
start |
state=COMPLETED와 voiceId 확인 |
화면 실행만으로 완료 아님. SET_REJECTED, PROFILE_FAILED, DELIVERY_FAILED는 상태로 처리 |
cancel |
대상 requestId의 state=CANCELLED, terminal=true |
Service 취소 사유는 USER_CANCELLED. 세션 없음·불일치는 무응답 가능 |
list |
success=true, profiles 읽기. 빈 배열도 성공 |
PROFILE_LIST_FAILED |
delete |
success=true, 응답 voiceId가 대상과 일치 |
ENROLLMENT_ACTIVE, PROFILE_NOT_FOUND, PROFILE_DELETE_FAILED |
interestsSet / interestsGet |
success=true, 응답 interestCodes로 화면 반영 |
FEATURE_DISABLED, INTERESTS_FAILED |
예시: 조회·삭제 왕복
설명용 기기 ID를 DEVICE001로 둔다. 실제 발행 시 해당 기기의 serialId로 바꾼다.
AWS Backend PUBLISH
skmg/airbot/DEVICE001/v1/command/voiceProfileEnrollment
action=list, payload.requestId=list-001
↓ IoT Core → IoTAgent → DeviceAgent → AI-Agent
AI-Agent: 기기에 저장된 프로필 조회
↓ DeviceAgent → IoTAgent
IoTAgent PUBLISH (제품 AWS 송신 연결은 구현 필요)
skmg/airbot/DEVICE001/v1/response/voiceProfileEnrollment/list-001
method=voiceProfileListResult, requestId=list-001
↓ IoT Core → AWS Backend → 모바일 App 목록 갱신
삭제 요청의 requestId가 delete-001이면 결과 토픽도 /delete-001로 끝난다. 아래는 삭제 성공·실패 본문의 각 예시다.
{
"contractVersion": "voice-profile-v2",
"method": "voiceProfileDeleteResult",
"requestId": "delete-001",
"success": true,
"voiceId": "vp-example-001",
"reasonCode": ""
}
{
"contractVersion": "voice-profile-v2",
"method": "voiceProfileDeleteResult",
"requestId": "delete-001",
"success": false,
"voiceId": "vp-example-001",
"reasonCode": "ENROLLMENT_ACTIVE"
}
두 예시는 서로 다른 결과다. 실패이면 목록에서 먼저 삭제하지 않고 기존 표시를 유지한다. 성공도 Cloud 사본까지 삭제됐다는 의미는 아니다.
7. 등록 상태 이벤트
| 등록 상태 필드 | 타입 | 설명 |
|---|---|---|
state |
string(enum) | WAITING_START, RECORDING_SET, SET_ACCEPTED, SET_REJECTED, PERSONALIZATION_SETTINGS, CREATING_LOCAL_PROFILE, PROFILE_FAILED, PENDING_DEVICE_DELIVERY, DELIVERY_FAILED, COMPLETED, CANCELLED |
stateSequence |
int64 | 같은 등록 요청의 상태 순번. 현재 생성 경로의 순번 역전 보완 필요 |
setIndex, setCount |
integer | 현재 세트와 전체 세트 수 |
progress |
integer | 진행률; Service 값을 그대로 전달 |
terminal |
boolean | 더 이상 진행하지 않는 상태인지 표시; 모든 실패가 true는 아님 |
nickname |
string | 표시 이름 |
voiceId |
string, 선택 | 생성 후 존재하는 화자 키 |
상태값별 의미
| 실제 enum 문자열 | 의미 | Backend·모바일 처리 |
|---|---|---|
WAITING_START |
사용자 시작 대기 | 등록 완료로 표시하지 않음 |
RECORDING_SET |
현재 세트 녹음 중 | setIndex/setCount·progress 반영 |
SET_ACCEPTED |
현재 세트 녹음 통과 | 전체 등록 완료와 구분 |
SET_REJECTED |
녹음 또는 세트 품질 거절 | reasonCode 확인, 기기 재녹음 안내 |
PERSONALIZATION_SETTINGS |
개인화 선택 대기 | 기기 화면에서 사용자 선택 대기 |
CREATING_LOCAL_PROFILE |
로컬 프로필 생성 중 | 처리 중 표시 |
PROFILE_FAILED |
프로필 생성 실패 | reasonCode와 terminal 확인 |
PENDING_DEVICE_DELIVERY |
DeviceAgent에 프로필 전달 대기 | Cloud 저장 완료로 표시하지 않음 |
DELIVERY_FAILED |
전달·저장 확인 실패 | reasonCode 확인, 단순 재등록 자동 실행 금지 |
COMPLETED |
등록 완료 상태 | voiceId 기준으로 목록 갱신. 로컬 시험 ACK 여부 별도 확인 |
CANCELLED |
등록 취소 | 대상 requestId의 등록 진행 표시 종료 |
등록 상태는 AI-Agent가 만든 뒤 DeviceAgent가 등록 App(대상 번호 8)과 IoTAgent(대상 번호 2)에 복사 전달한다. 이 번호는 토픽 구간이나 JSON 필드가 아니다. 기기 내부 상태 분배 예시.
{
"contractVersion": "voice-profile-v2",
"method": "voiceProfileStateChanged",
"requestId": "vp-example-001",
"success": false,
"state": "RECORDING_SET",
"stateSequence": 2,
"setIndex": 1,
"setCount": 5,
"progress": 20,
"terminal": false,
"nickname": "Niky",
"reasonCode": ""
}
8. 오류·중복·연결 복구
| 상황 | 현재 | 제품 처리 요구 |
|---|---|---|
| 잘못된 버전·만료·payload 금지 필드 | 로그 후 return, MQTT 오류 응답 없음 | 요청 ID를 확인할 수 있는 요청은 명시적 거절 결과 반환 |
| DeviceAgent Binder 반환 음수 | 로그 후 return | 전달 실패를 Backend에 반환 |
| Broker 연결 끊김 | 시험 Bridge publishResult는 즉시 return | 결과 보관·재전송; 완료 이벤트 유실 방지 |
| 같은 start 재전달 | 세트 초기화 가능 | 중복 실행 차단 후 현재 상태 응답 |
| 다른 등록과 충돌 | UI 경로에서 무응답 가능 | 기존 세션 보존·명시적 실패 응답 |
| 생성 중 취소 | 늦은 생성 완료와 경합 | AI-Agent 보완 필요; MQTT 전달만으로 해결 불가 |
| 상태 역전 | UI는 이전 이하 순번 무시 | Service의 순번 증가 보완 |
| 삭제 성공 | 로컬 처리 결과; 대기 파일 제거 실패가 가려질 수 있음 | 부분 실패·Cloud 삭제 별도 확인 |
MQTT PUBACK는 Broker 전달 확인이고 등록 완료·Cloud 저장 확인이 아니다. AWS는 QoS 0/1을 지원하며 QoS 1에는 재전달이 있으므로 중복 실행 방지가 필요하다. AWS MQTT
권장 Retain=false로 이전 등록/삭제 명령이 나중에 재실행되는 것을 피한다. expiresAtMs도 반드시 전송한다. 시험 Bridge의 cleanSession=true·메모리 저장·자동 재연결은 제품의 오프라인 결과 보관을 대신하지 못한다. timeout만으로 실패한 작업을 다시 start하지 말고 상태/목록을 확인한다.
9. 임베딩 업로드와 Cloud ACK
등록 임베딩은 Cloud로 보내야 한다. 다만 현재 commitVoiceProfile은 Binder 메서드이며 MQTT 명령이 아니다.
현재 경로는 AI-Agent → DeviceAgent 암호화 대기 저장이다. LOCAL_TEST_ACK는 로컬 시험 완료이고, WAITING_CLOUD_SYNC는 Cloud 미연결이다. 기존 MQTT Bridge에 임베딩 배열을 넣어도 제품 저장이 구현되지 않는다.
| 확정할 항목 | 담당 | 필요한 내용 |
|---|---|---|
| 업로드 토픽·JSON 형식 | IoTAgent·AWS Backend | Binder 바이트를 JSON 숫자 배열로 임의 변환하지 않음. 인코딩·모델 버전·차원·해시·크기 정의 |
| 업로드 수신·저장 | AWS Backend | 실제 voiceId·기기·계정 연결, 암호화 저장·접근 통제 |
| 저장 ACK | AWS Backend·DeviceAgent·IoTAgent | 성공/실패, requestId·voiceId 대응, ACK 명령·토픽 확정 |
| 늦은 ACK·재전송 | AI-Agent·DeviceAgent·IoTAgent | 취소·재시작 후 ACK 처리, 중복 저장 방지, 재부팅 후 재전송 |
| 삭제 | AWS Backend·DeviceAgent | 로컬/대기/Cloud 사본별 삭제 확인 |
AWS MQTT payload 한도는 128 KB다. 제품 인코딩 이후 크기를 검사하고 초과 시 별도 업로드 방식이나 분할 계약을 먼저 정의한다. AWS MQTT 크기 제한
원본 PCM/WAV와 인식용 Query Embedding은 업로드하지 않는다. 명령 입력의 audio/pcm/wav/embedding/referenceEmbedding 필드는 현재 거절된다. 등록 임베딩의 상향 전송과 명령 입력 금지를 혼동하지 않는다.
10. AWS 설정과 책임
| 담당 | 해야 할 일 |
|---|---|
| SoC IoTAgent | 기존 AWS 연결 재사용, 자기 기기 명령 구독, action 변환, 상태·결과 publish, 연결 복구 |
| DeviceAgent | Binder 전달·callback 배분, 임베딩 대기 저장, 업로드/ACK 중계 추가 |
| AWS Backend | 명령 발행 전 권한 확인, requestId 관리, 결과 수신/저장, 모바일 알림, Cloud ACK |
| AWS 운영 | 기기 인증서·IoT Policy·Rule·저장 처리 권한 설정 및 로그 |
| 등록 App·AI-Agent | 녹음·상태·결과 생성과 취소/중복/순번 보완; AWS 연결은 소유하지 않음 |
기기 인증서는 자기 기기 범위만 허용한다. iot:Subscribe는 topicfilter ARN, iot:Receive와 iot:Publish는 topic ARN을 사용한다. MQTT의 +/#와 IAM 정책의 */?는 다른 문법이다. AWS 발행·구독 정책
Backend는 MQTT 구독 프로세스 또는 IoT Rule→처리 서비스 중 하나를 주 수신 경로로 선택한다. 둘 다 사용하면 중복 저장을 방지해야 한다. 모바일에 기기 인증서나 MQTT 비밀값을 넣지 않는다.
11. 인수 시험
시나리오별 요청·결과 예시
아래는 메시지 형식 설명용이며 실제 AWS 시험 로그가 아니다. 모든 요청은 Backend가 skmg/airbot/DEVICE001/v1/command/voiceProfileEnrollment에 발행한다. 기기 ID와 voiceId는 실제 값으로 바꾼다. 시각 0은 실행 전에 issuedAtMs=현재 Unix 밀리초, expiresAtMs=issuedAtMs+60000으로 바꾼다. 60초는 예시 유효기간이며 서버에 고정된 기본값이 아니다.
결과는 IoTAgent가 skmg/airbot/DEVICE001/v1/response/voiceProfileEnrollment/{requestId}로 발행하는 형태다. 현재 제품 AWS publish 연결은 구현 대상이며 로컬 Bridge 형식을 기준으로 설명한다.
A. 등록 요청 → 기기에서 녹음 시작
Backend 발행:
{
"action": "start",
"payload": {
"contractVersion": "voice-profile-v2",
"method": "startVoiceProfileEnrollment",
"requestId": "register-001",
"nickname": "Niky",
"locale": "ko-KR",
"personalizationEnabled": true,
"issuedAtMs": 0,
"expiresAtMs": 0
}
}
이 요청은 기기 등록 화면을 연다. 사용자가 시작 버튼을 눌러 녹음을 시작하면 /register-001에서 다음 상태를 받는다.
{
"contractVersion": "voice-profile-v2",
"method": "voiceProfileStateChanged",
"requestId": "register-001",
"success": false,
"state": "RECORDING_SET",
"stateSequence": 2,
"setIndex": 1,
"setCount": 5,
"progress": 0,
"terminal": false,
"nickname": "Niky",
"reasonCode": ""
}
모바일은 등록 진행 중으로 표시한다. 이 상태의 success=false는 시험 Bridge가 추가한 값이며 녹음 실패를 뜻하지 않는다. 아직 voiceId가 없을 수 있다.
B. 등록 취소 → 취소 상태
진행 중인 등록과 같은 requestId를 보낸다.
{
"action": "cancel",
"payload": {
"contractVersion": "voice-profile-v2",
"method": "cancelVoiceProfileEnrollment",
"requestId": "register-001",
"reason": "MOBILE_CANCELLED",
"issuedAtMs": 0,
"expiresAtMs": 0
}
}
대상 세션에서 취소가 처리된 경우 /register-001의 결과:
{
"contractVersion": "voice-profile-v2",
"method": "voiceProfileStateChanged",
"requestId": "register-001",
"success": false,
"state": "CANCELLED",
"stateSequence": 3,
"setIndex": 1,
"setCount": 5,
"progress": 0,
"terminal": true,
"nickname": "Niky",
"reasonCode": "USER_CANCELLED"
}
모바일은 해당 등록의 진행 표시를 종료한다. reason 입력과 결과 reasonCode는 서로 다른 필드다. 세션 없음·ID 불일치·화면 실행 실패 시 현재는 결과가 오지 않을 수 있다.
C. 관심사 저장 → 저장된 목록 확인
아래는 이전 MQTT 저장 예제다. 현재 모바일 저장은 서버 HTTPS PUT이며 기기로 전송하지 않는다.
{
"action": "interestsSet",
"payload": {
"contractVersion": "voice-memory-v1",
"method": "setVoiceProfileInterests",
"requestId": "interests-save-001",
"voiceId": "vp-example-001",
"interestCodes": [
"WEATHER",
"HEALTH"
],
"issuedAtMs": 0,
"expiresAtMs": 0
}
}
/interests-save-001의 성공 결과:
{
"contractVersion": "voice-memory-v1",
"method": "voiceProfileInterestsResult",
"requestId": "interests-save-001",
"success": true,
"reasonCode": "",
"voiceId": "vp-example-001",
"interestCodes": [
"WEATHER",
"HEALTH"
]
}
모바일은 응답의 voiceId에 해당하는 관심사를 갱신한다. 기존 관심사에 추가하는 것이 아니라 위 목록으로 교체한다.
D. 관심사 조회 → 해당 화자 값 표시
아래는 이전 MQTT 조회 예제다. 현재 모바일 GET은 서버 DB를 읽고, AI-Agent 조회는 신규 getVoicePersonalization을 사용한다.
{
"action": "interestsGet",
"payload": {
"contractVersion": "voice-memory-v1",
"method": "getVoiceProfileInterests",
"requestId": "interests-read-001",
"voiceId": "vp-example-001",
"issuedAtMs": 0,
"expiresAtMs": 0
}
}
/interests-read-001의 결과:
{
"contractVersion": "voice-memory-v1",
"method": "voiceProfileInterestsResult",
"requestId": "interests-read-001",
"success": true,
"reasonCode": "",
"voiceId": "vp-example-001",
"interestCodes": [
"WEATHER",
"HEALTH"
]
}
저장과 조회의 결과 method는 동일하다. 어떤 요청의 결과인지는 requestId로 구분한다. 빈 목록 []도 성공 결과가 될 수 있다.
검증 목록
| 시험 | 통과 기준 |
|---|---|
| 명령·응답 왕복 | 같은 기기·requestId가 command JSON, Binder, response 토픽/JSON에 일치 |
| start/cancel/list/delete | 각각 상태/목록/삭제 결과까지 확인. PUBLISH 성공만으로 완료 판정하지 않음 |
| 상태 success=false | 상태 이벤트의 success=false를 실패로 오판하지 않음 |
| 오입력·만료 | 명시적 실패 또는 현재 무응답 한계 기록, 자동 재실행 없음 |
| 중복·취소 경합 | 중복 녹음/생성 없음, 취소 후 늦은 완료 부활 없음 (Service 보완 후) |
| 네트워크·재부팅 | 결과 유실/중복 저장 없이 복구 (제품 보완 후) |
| Cloud 등록 | 실제 임베딩 저장과 저장 ACK 확인; 로컬 ACK와 구분 |
| 권한 | 다른 기기의 command/response 접근 거절 |
이 목록은 제품 완료 조건이며 실기기/AWS 시험 통과 보고서가 아니다.
구현 근거와 연결 문서
2026-09-15 작업본 기준: SoC speaker_identification HEAD e54419a, AI-Agent feature/cpu-speaker-recognition HEAD 091e52bc 및 미커밋 구현 포함.
- IoTAgent
mqtt/VoiceProfileContract.java: action/method/키 상수. - IoTAgent
mqtt/topic/CmdTopic.java: handleVoiceProfileEnrollment, buildVoiceMemoryCommand. - IoTAgent
mqtt/VoiceProfileMqttTestBridge.java: commandTopic, responseTopic, publishResult, toJsonPayload. - IoTAgent
IotAgent.java: 관리/실시간 callback을 시험 Bridge에 전달하는 분기. - AI-Agent
service/VoiceProfileIpcContract.kt: 등록 상태 Bundle 생성. - 전체 프로토콜과 시나리오
- DeviceAgent 함수·Bundle 명세
- 모바일·Backend HTTPS 명세