Xylolabs API 레퍼런스¶
버전: v1 최종 수정: 2026-03-22
목차¶
- 개요
- 인증
- 시설 관리
- 사용자 관리
- API 키
- 디바이스
- 오디오 업로드
- 오디오 스트리밍
- 트랜스코딩 작업
- 태그
- 메타데이터 수집
- 메타데이터 조회
- 시스템 설정
- 대시보드 통계
- 헬스 체크
- XMBP 프로토콜 레퍼런스
- RBAC 레퍼런스
- 오류 응답
- 데이터 모델
- 페이지네이션
- 워크플로 예제
- LTE Cat-M1 통합 가이드
1. 개요¶
Xylolabs API란?¶
Xylolabs API는 IoT/임베디드 디바이스의 오디오와 센서 메타데이터를 통합 관리하는 백엔드 서비스다. Pico, ESP32, Cat-M1 모뎀 등 현장 장비가 데이터를 보내면 서버가 저장, 변환, 조회를 처리한다.
핵심 기능은 다음과 같다.
- 오디오 업로드 및 트랜스코딩: WAV, FLAC 등 원본 파일을 업로드하면 Opus/WebM, AAC/MP4 등 웹 재생용 포맷으로 자동 변환한다.
- 실시간 메타데이터 스트리밍: XMBP 바이너리 프로토콜로 센서 데이터를 HTTP 또는 WebSocket으로 전송한다. LTE Cat-M1처럼 대역폭이 제한된 환경에 맞춰 설계했다.
- 멀티테넌트 시설 격리: 모든 데이터를 시설(Facility) 단위로 격리하고 역할 기반 접근 제어(RBAC)를 적용한다.
- 관리 대시보드 API: 시설, 사용자, API 키, 디바이스, 시스템 설정의 CRUD를 지원한다.
빠른 시작¶
5분 안에 첫 데이터를 전송하려면 아래 순서를 따른다.
# Step 1: Log in and grab a token
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@xylolabs.com","password":"changeme"}' \
| jq -r '.access_token')
# Step 2: Create a facility
FACILITY_ID=$(curl -s -X POST http://localhost:3000/api/v1/facilities \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"페리지에어로스페이스 옥천공장","slug":"perigee-okcheon"}' \
| jq -r '.id')
# Step 3: Create an API key for device ingestion
API_KEY=$(curl -s -X POST http://localhost:3000/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"터빈홀 센서 어레이","scopes":["upload","ingest"]}' \
| jq -r '.key')
# Step 4: Upload your first audio file
curl -X POST http://localhost:3000/api/v1/uploads \
-H "X-Api-Key: $API_KEY" \
-F "file=@turbine_hall_2025-06-15_14h30.wav" \
-F 'metadata={"location":"turbine_hall","sample_rate":384000}'
echo "Done -- check the dashboard to see your upload."
아키텍처¶
임베디드 디바이스 (Pico, ESP32, Cat-M1 모뎀 등)
|
+-- 오디오 업로드 (multipart HTTP) ---> S3 (원본) ---> 자동 트랜스코딩 ---> S3 (웹 재생용)
|
+-- 메타데이터 스트리밍 (XMBP) -------> IngestManager ---> PostgreSQL + S3 청크
|
+-- LTE Cat-M1 (AT 명령 HTTP POST) ---> 위와 동일 경로 (저대역폭 최적화)
관리 대시보드 (웹)
|
+-- JWT 인증 ------> REST API --------> PostgreSQL / S3
기본 URL¶
| 환경 | Base URL |
|---|---|
| 프로덕션 API | https://api.xylolabs.com/api |
| 관리 대시보드 | https://admin.api.xylolabs.com |
| 개발 환경 | http://localhost:3000/api |
인증 방식¶
| 방식 | 헤더 | 사용 주체 | 대상 엔드포인트 |
|---|---|---|---|
| JWT Bearer Token | Authorization: Bearer <token> |
대시보드, 웹 클라이언트 | 모든 /api/v1/* 보호 경로 |
| API Key | X-Api-Key: xk_... |
임베디드 디바이스 | 업로드, 수집 |
2. 인증¶
POST /api/auth/login¶
이메일과 비밀번호로 로그인한다. 성공하면 JWT access token과 refresh token 쌍을 돌려준다.
인증: 없음 필요 역할: 없음
요청 본문:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
email |
string | O | 사용자 이메일 |
password |
string | O | 비밀번호 |
응답 200 OK:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "minseok.jeon@xylolabs.com",
"display_name": "전민석",
"role": "super_admin",
"facility_id": null
}
}
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
401 Unauthorized |
이메일 또는 비밀번호가 올바르지 않음 |
401 Unauthorized |
비활성 계정 |
curl:
# Log in as super admin
curl -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "minseok.jeon@xylolabs.com",
"password": "your-password"
}'
POST /api/auth/refresh¶
리프레시 토큰으로 access token을 갱신하고 새 리프레시 토큰으로 회전한다. access token이 만료됐을 때 재로그인 없이 새 토큰을 받을 수 있다.
인증: 없음 필요 역할: 없음
요청 본문:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
refresh_token |
string | O | 로그인 시 발급받은 리프레시 토큰 |
응답 200 OK:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
401 Unauthorized |
유효하지 않거나 만료된 리프레시 토큰 |
401 Unauthorized |
사용자를 찾을 수 없거나 비활성 상태 |
curl:
# Refresh an expired access token
curl -X POST https://api.xylolabs.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"eyJhbGciOiJIUzI1NiIs..."}'
GET /api/auth/me¶
현재 로그인한 사용자 정보를 돌려준다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
응답 200 OK:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "minseok.jeon@xylolabs.com",
"display_name": "전민석",
"role": "super_admin",
"facility_id": null
}
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
401 Unauthorized |
JWT가 없거나 유효하지 않음 |
curl:
# Check who you are logged in as
curl https://api.xylolabs.com/api/auth/me \
-H "Authorization: Bearer $TOKEN"
3. 시설 관리¶
시설(Facility)은 Xylolabs의 최상위 조직 단위다. 사용자, 디바이스, 업로드, 수집 세션 등 모든 데이터가 특정 시설에 소속되며, 시설 간 데이터는 완전히 격리된다. 시설 CRUD는 슈퍼 관리자만 할 수 있다.
GET /api/v1/facilities¶
등록된 전체 시설 목록을 돌려준다.
인증: JWT Bearer Token
필요 역할: super_admin
응답 200 OK:
[
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링",
"is_active": true,
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-01-15T09:00:00Z"
},
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "페리지에어로스페이스 미래기술연구소",
"slug": "perigee-futuretech",
"description": "미래기술연구소 센서 데이터 수집",
"is_active": true,
"created_at": "2025-02-20T14:00:00Z",
"updated_at": "2025-02-20T14:00:00Z"
}
]
curl:
# List all facilities (super_admin only)
curl https://api.xylolabs.com/api/v1/facilities \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/facilities¶
새 시설을 만든다.
인증: JWT Bearer Token
필요 역할: super_admin
요청 본문:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
string | O | 시설 표시 이름 |
slug |
string | O | URL에 사용할 고유 식별자 (영문, 하이픈) |
description |
string | X | 시설 설명 |
응답 200 OK:
{
"id": "660e8400-e29b-41d4-a716-446655440002",
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링",
"is_active": true,
"created_at": "2025-06-15T10:00:00Z",
"updated_at": "2025-06-15T10:00:00Z"
}
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
슈퍼 관리자가 아닌 경우 |
409 Conflict |
동일한 slug가 이미 존재 |
curl:
# Create a new wildlife monitoring facility
curl -X POST https://api.xylolabs.com/api/v1/facilities \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링"
}'
GET /api/v1/facilities/{id}¶
ID로 시설 하나를 조회한다.
인증: JWT Bearer Token
필요 역할: super_admin
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 시설 ID |
응답 200 OK:
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링",
"is_active": true,
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-01-15T09:00:00Z"
}
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
슈퍼 관리자가 아닌 경우 |
404 Not Found |
해당 ID의 시설이 없음 |
curl:
# Fetch a single facility by ID
curl https://api.xylolabs.com/api/v1/facilities/660e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN"
PATCH /api/v1/facilities/{id}¶
시설 정보를 부분 수정한다. 보낸 필드만 업데이트되고 나머지는 그대로 유지된다.
인증: JWT Bearer Token
필요 역할: super_admin
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 시설 ID |
요청 본문:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
string | X | 새 시설 이름 |
description |
string | X | 새 설명 |
is_active |
boolean | X | 활성화/비활성화 토글 |
응답 200 OK: 수정된 시설 객체 (GET과 동일한 형식).
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
슈퍼 관리자가 아닌 경우 |
404 Not Found |
해당 ID의 시설이 없음 |
curl:
# Rename a facility
curl -X PATCH https://api.xylolabs.com/api/v1/facilities/660e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"페리지에어로스페이스 옥천공장 (2호동)"}'
DELETE /api/v1/facilities/{id}¶
시설을 삭제한다.
인증: JWT Bearer Token
필요 역할: super_admin
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 시설 ID |
응답 204 No Content
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
슈퍼 관리자가 아닌 경우 |
404 Not Found |
해당 ID의 시설이 없음 |
curl:
# Delete a facility permanently
curl -X DELETE https://api.xylolabs.com/api/v1/facilities/660e8400-e29b-41d4-a716-446655440002 \
-H "Authorization: Bearer $TOKEN"
4. 사용자 관리¶
사용자는 시설에 소속되며 역할에 따라 접근 권한이 달라진다. 시설 관리자는 자기 시설 안의 사용자만 관리할 수 있고, 슈퍼 관리자는 전체 사용자를 관리한다.
GET /api/v1/users¶
사용자 목록을 조회한다. 시설 관리자라면 자기 시설의 사용자만, 슈퍼 관리자라면 전체 사용자가 표시된다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
응답 200 OK:
[
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"email": "minseok.jeon@xylolabs.com",
"display_name": "전민석",
"role": "user",
"is_active": true,
"last_login_at": "2025-06-15T08:30:00Z",
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-06-15T08:30:00Z"
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"email": "dongyun.shin@xylolabs.com",
"display_name": "신동윤",
"role": "facility_admin",
"is_active": true,
"last_login_at": "2025-06-14T17:45:00Z",
"created_at": "2025-02-01T10:00:00Z",
"updated_at": "2025-06-14T17:45:00Z"
}
]
curl:
# List users visible to the current account
curl https://api.xylolabs.com/api/v1/users \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/users¶
새 사용자를 만든다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
요청 본문:
{
"email": "minseok.jeon@xylolabs.com",
"password": "Xylo#2025secure",
"display_name": "전민석",
"role": "user",
"facility_id": "660e8400-e29b-41d4-a716-446655440000"
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
email |
string | O | 고유한 이메일 주소 |
password |
string | O | 비밀번호 (서버에서 Argon2로 해시 처리) |
display_name |
string | O | 표시 이름 |
role |
string | O | super_admin, facility_admin, user 중 하나 |
facility_id |
UUID | X | 소속 시설 (슈퍼 관리자만 지정 가능; 시설 관리자는 자기 시설에 자동 배정) |
응답 200 OK: 생성된 사용자 객체 (목록 항목과 동일한 형식).
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
권한 부족 |
409 Conflict |
이메일이 이미 등록되어 있음 |
curl:
# Create a read-only user in the facility
curl -X POST https://api.xylolabs.com/api/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "dongyun.shin@xylolabs.com",
"password": "Xylo#2025secure",
"display_name": "신동윤",
"role": "user",
"facility_id": "660e8400-e29b-41d4-a716-446655440000"
}'
GET /api/v1/users/{id}¶
ID로 사용자 한 명을 조회한다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 사용자 ID |
응답 200 OK: 사용자 객체.
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
권한 부족 또는 다른 시설의 사용자에 접근 |
404 Not Found |
해당 사용자가 없음 |
curl:
# Get a specific user
curl https://api.xylolabs.com/api/v1/users/550e8400-e29b-41d4-a716-446655440001 \
-H "Authorization: Bearer $TOKEN"
PATCH /api/v1/users/{id}¶
사용자 정보를 부분 수정한다. 보낸 필드만 변경된다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 사용자 ID |
요청 본문:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
display_name |
string | X | 새 표시 이름 |
role |
string | X | 새 역할 |
is_active |
boolean | X | 활성화/비활성화 |
응답 200 OK: 수정된 사용자 객체.
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
권한 부족 또는 다른 시설의 사용자에 접근 |
404 Not Found |
해당 사용자가 없음 |
curl:
# Promote a user to facility_admin
curl -X PATCH https://api.xylolabs.com/api/v1/users/550e8400-e29b-41d4-a716-446655440001 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"role":"facility_admin"}'
DELETE /api/v1/users/{id}¶
사용자를 비활성화한다. 실제 레코드를 지우지 않고 is_active를 false로 바꾸는 소프트 삭제 방식이다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 사용자 ID |
응답 204 No Content
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
권한 부족 또는 다른 시설의 사용자에 접근 |
404 Not Found |
해당 사용자가 없음 |
curl:
# Soft-delete (deactivate) a user
curl -X DELETE https://api.xylolabs.com/api/v1/users/550e8400-e29b-41d4-a716-446655440001 \
-H "Authorization: Bearer $TOKEN"
5. API 키¶
API 키는 임베디드 디바이스가 오디오 업로드와 메타데이터 수집에 쓰는 인증 수단이다. 각 키는 특정 시설에 묶이고, 스코프와 만료일을 세밀하게 설정할 수 있다. 평문 키는 생성 시 딱 한 번만 표시되고 다시 조회 가능한 형태로 저장되지 않으므로 즉시 안전한 곳에 저장한다.
GET /api/v1/api-keys¶
현재 사용자 시설의 API 키 전체를 반환한다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
응답 200 OK:
[
{
"id": "770e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"key_prefix": "xk_a1b2",
"name": "터빈홀 센서 어레이",
"scopes": ["upload", "ingest"],
"is_active": true,
"expires_at": "2026-01-01T00:00:00Z",
"last_used_at": "2025-06-15T10:30:00Z",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-01T09:00:00Z"
},
{
"id": "770e8400-e29b-41d4-a716-446655440001",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"key_prefix": "xk_c3d4",
"name": "수중 하이드로폰 #3",
"scopes": ["ingest"],
"is_active": true,
"expires_at": null,
"last_used_at": "2025-06-14T22:15:00Z",
"created_by": "550e8400-e29b-41d4-a716-446655440002",
"created_at": "2025-05-10T11:00:00Z"
}
]
curl:
# List all API keys for your facility
curl https://api.xylolabs.com/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/api-keys¶
새 API 키를 발급한다. 응답의 key 필드에 평문 키가 포함되며, 이 값은 이후 다시 조회할 수 없다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
요청 본문:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
string | O | 키를 구별할 이름 |
scopes |
string[] | X | 권한 스코프 (생략 시 전체 권한) |
expires_at |
ISO 8601 | X | 만료일 (null = 무기한) |
응답 200 OK:
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"facility_id": "660e8400-e29b-41d4-a716-446655440002",
"key_prefix": "xk_e5f6",
"name": "조류 음성 레코더",
"scopes": ["upload", "ingest"],
"expires_at": "2026-06-01T00:00:00Z",
"created_at": "2025-06-15T10:00:00Z",
"key": "xk_e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6"
}
주의:
key필드는 이 응답에서만 볼 수 있다. 분실하면 키를 폐기하고 새로 발급해야 한다.
curl:
# Create an API key for a field recorder with 1-year expiry
curl -X POST https://api.xylolabs.com/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "조류 음성 레코더",
"scopes": ["upload","ingest"],
"expires_at": "2026-06-01T00:00:00Z"
}'
DELETE /api/v1/api-keys/{id}¶
API 키를 폐기한다. 폐기된 키로는 더 이상 인증할 수 없다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | API 키 ID |
응답 204 No Content
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
권한 부족 또는 다른 시설의 키에 접근 |
curl:
# Revoke a compromised API key
curl -X DELETE https://api.xylolabs.com/api/v1/api-keys/770e8400-e29b-41d4-a716-446655440002 \
-H "Authorization: Bearer $TOKEN"
6. 디바이스¶
디바이스는 시설에 배치된 물리 장비(RPi Pico 2, ESP32, Cat-M1 모뎀 등)를 나타낸다. 시설 안에서 device_uid 정수로 구별하고, 대시보드에서 직접 등록하거나 수집 세션이 새 device_uid를 참조하면 자동으로 생성된다.
GET /api/v1/devices¶
현재 사용자 시설의 디바이스 전체를 반환한다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
응답 200 OK:
[
{
"id": "cc0e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"device_uid": 1,
"name": "터빈홀 센서 어레이",
"hardware_version": "rp2350",
"firmware_version": "1.2.0",
"is_active": true,
"last_seen_at": "2025-06-15T10:30:00Z",
"created_at": "2025-06-01T09:00:00Z"
},
{
"id": "cc0e8400-e29b-41d4-a716-446655440001",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"device_uid": 2,
"name": "수중 하이드로폰 #3",
"hardware_version": "esp32-s3",
"firmware_version": "2.0.1",
"is_active": true,
"last_seen_at": "2025-06-15T09:45:00Z",
"created_at": "2025-04-10T13:00:00Z"
}
]
curl:
# List devices in your facility
curl https://api.xylolabs.com/api/v1/devices \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/devices¶
새 디바이스를 등록한다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
요청 본문:
{
"device_uid": 3,
"name": "조류 음성 레코더",
"hardware_version": "nrf9160",
"firmware_version": "0.9.0"
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
device_uid |
u32 | O | 시설 안에서 유일한 디바이스 번호 |
name |
string | O | 디바이스 표시 이름 |
hardware_version |
string | X | 하드웨어 버전 |
firmware_version |
string | X | 펌웨어 버전 |
응답 201 Created:
{
"id": "cc0e8400-e29b-41d4-a716-446655440002",
"facility_id": "660e8400-e29b-41d4-a716-446655440002",
"device_uid": 3,
"name": "조류 음성 레코더",
"hardware_version": "nrf9160",
"firmware_version": "0.9.0",
"is_active": true,
"last_seen_at": null,
"created_at": "2025-06-15T10:00:00Z"
}
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
권한 부족 |
409 Conflict |
같은 시설에 동일한 device_uid가 이미 존재 |
curl:
# Register a new Cat-M1 field recorder
curl -X POST https://api.xylolabs.com/api/v1/devices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"device_uid": 3,
"name": "조류 음성 레코더",
"hardware_version": "nrf9160",
"firmware_version": "0.9.0"
}'
GET /api/v1/devices/{id}¶
ID로 디바이스 하나를 조회한다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 디바이스 ID |
응답 200 OK: 디바이스 객체.
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
다른 시설의 디바이스에 접근 |
404 Not Found |
해당 디바이스가 없음 |
curl:
# Get device details
curl https://api.xylolabs.com/api/v1/devices/cc0e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN"
PATCH /api/v1/devices/{id}¶
디바이스 정보를 부분 수정한다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 디바이스 ID |
요청 본문:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
string | X | 새 디바이스 이름 |
hardware_version |
string | X | 새 하드웨어 버전 |
firmware_version |
string | X | 새 펌웨어 버전 |
is_active |
boolean | X | 활성화/비활성화 |
응답 200 OK: 수정된 디바이스 객체.
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
권한 부족 또는 다른 시설의 디바이스에 접근 |
404 Not Found |
해당 디바이스가 없음 |
curl:
# Update firmware version after OTA
curl -X PATCH https://api.xylolabs.com/api/v1/devices/cc0e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"firmware_version":"1.3.0"}'
7. 오디오 업로드¶
POST /api/v1/uploads¶
multipart form data로 오디오 파일을 업로드한다. 서버가 원본을 S3에 저장한 뒤, 웹 재생에 맞는 포맷으로 트랜스코딩 작업을 자동 생성한다.
인증: API Key (X-Api-Key 헤더)
필요 역할: 해당 없음 (API Key로 시설 범위 지정)
Content-Type: multipart/form-data
Multipart 필드:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
file |
file | O | 오디오 파일 (WAV, FLAC, MP3 등) |
metadata |
text (JSON) | X | 부가 메타데이터 JSON |
tags |
text (JSON) | X | 태그 UUID 배열 |
파일 크기 참고: 서버에서 multipart 요청의 최대 크기를 제한할 수 있다. Cat-M1 같은 저대역폭 환경에서는 파일을 잘라서 올리거나 XMBP 스트리밍으로 전환하는 것이 좋는다. 384kHz/24bit WAV 1분 분량은 약 138MB이므로 Cat-M1으로는 1시간 이상 걸릴 수 있다.
응답 200 OK:
{
"id": "880e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"api_key_id": "770e8400-e29b-41d4-a716-446655440000",
"uploaded_by": null,
"original_filename": "turbine_hall_2025-06-15_14h30.wav",
"original_format": "wav",
"original_size_bytes": 115200000,
"duration_ms": null,
"sample_rate_hz": null,
"channels": null,
"bit_depth": null,
"codec": null,
"metadata": {"location": "turbine_hall", "sample_rate": 384000, "bit_depth": 24},
"status": "pending",
"tags": [],
"created_at": "2025-06-15T14:30:00Z",
"updated_at": "2025-06-15T14:30:00Z"
}
업로드 상태: pending(대기) -> processing(변환 중) -> completed(완료) 또는 failed(실패).
curl:
# Upload a high-res WAV recording with metadata and tags
curl -X POST https://api.xylolabs.com/api/v1/uploads \
-H "X-Api-Key: xk_a1b2c3d4e5f6..." \
-F "file=@turbine_hall_2025-06-15_14h30.wav" \
-F 'metadata={"location":"turbine_hall","sample_rate":384000,"bit_depth":24}' \
-F 'tags=["990e8400-e29b-41d4-a716-446655440000"]'
Python 업로드 예제:
import requests
API_KEY = "xk_a1b2c3d4e5f6..."
BASE_URL = "https://api.xylolabs.com/api/v1"
# Upload a FLAC file with metadata
with open("hydrophone_reef_sample.flac", "rb") as f:
resp = requests.post(
f"{BASE_URL}/uploads",
headers={"X-Api-Key": API_KEY},
files={"file": ("hydrophone_reef_sample.flac", f, "audio/flac")},
data={
"metadata": '{"location":"reef_zone_alpha","depth_m":12.5}',
"tags": '["990e8400-e29b-41d4-a716-446655440000"]',
},
)
upload = resp.json()
print(f"Upload ID: {upload['id']}, status: {upload['status']}")
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
400 Bad Request |
file 필드 누락 또는 잘못된 multipart |
401 Unauthorized |
유효하지 않거나 누락된 API 키 |
저장 흐름:
GET /api/v1/uploads¶
시설의 업로드 목록을 페이지네이션으로 조회한다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
쿼리 파라미터:
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
page |
integer | 1 | 페이지 번호 (1부터 시작) |
per_page |
integer | 20 | 페이지당 항목 수 (1-100) |
status |
string | -- | 상태 필터: pending, processing, completed, failed |
응답 200 OK:
{
"items": [
{
"id": "880e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"original_filename": "turbine_hall_2025-06-15_14h30.wav",
"original_format": "wav",
"original_size_bytes": 115200000,
"status": "completed",
"tags": [
{
"id": "990e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "터빈 진동",
"color": "#F44336",
"created_at": "2025-06-01T09:00:00Z"
}
],
"created_at": "2025-06-15T14:30:00Z",
"updated_at": "2025-06-15T14:35:00Z"
},
{
"id": "880e8400-e29b-41d4-a716-446655440001",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"original_filename": "hydrophone_reef_sample.flac",
"original_format": "flac",
"original_size_bytes": 48000000,
"status": "completed",
"tags": [],
"created_at": "2025-06-14T22:00:00Z",
"updated_at": "2025-06-14T22:03:00Z"
}
],
"total": 42,
"page": 1,
"per_page": 20
}
curl:
# Get the first page of completed uploads
curl "https://api.xylolabs.com/api/v1/uploads?page=1&per_page=10&status=completed" \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/uploads/{id}¶
업로드 하나를 상세 조회한다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 업로드 ID |
응답 200 OK: 전체 업로드 객체 (목록 항목과 동일한 스키마).
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
다른 시설의 업로드에 접근 |
404 Not Found |
해당 업로드가 없음 |
curl:
# Get details of a specific upload
curl https://api.xylolabs.com/api/v1/uploads/880e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN"
PATCH /api/v1/uploads/{id}¶
업로드의 메타데이터나 태그를 수정한다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 업로드 ID |
요청 본문:
{
"metadata": {
"location": "turbine_hall_east",
"sample_rate": 384000,
"notes": "East side sensor replaced 2025-06-15"
},
"tags": ["990e8400-e29b-41d4-a716-446655440000"]
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
metadata |
object | X | 메타데이터 전체 교체 (기존 값을 덮어씀) |
tags |
UUID[] | X | 태그 전체 교체 (기존 태그 제거 후 새 태그 연결) |
응답 200 OK: 수정된 업로드 객체.
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
다른 시설의 업로드에 접근 |
404 Not Found |
해당 업로드가 없음 |
curl:
# Update upload metadata and tags
curl -X PATCH https://api.xylolabs.com/api/v1/uploads/880e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"metadata": {"location":"turbine_hall_east","notes":"East side sensor replaced"},
"tags": ["990e8400-e29b-41d4-a716-446655440000"]
}'
DELETE /api/v1/uploads/{id}¶
업로드를 완전히 삭제한다. S3에서 파일을 지우고 DB 레코드를 삭제하며, 연결된 트랜스코딩 작업과 태그 연결도 함께 삭제된다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 업로드 ID |
응답 204 No Content
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
다른 시설의 업로드에 접근 |
404 Not Found |
해당 업로드가 없음 |
curl:
# Permanently delete an upload and its transcoded copies
curl -X DELETE https://api.xylolabs.com/api/v1/uploads/880e8400-e29b-41d4-a716-446655440001 \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/uploads/{id}/retranscode¶
기존 업로드를 현재 시스템 설정 기준으로 다시 트랜스코딩한다. 기본 출력 포맷을 변경한 뒤 일괄 재변환할 때 쓸 수 있다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 업로드 ID |
응답 200 OK:
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
다른 시설의 업로드에 접근 |
404 Not Found |
해당 업로드가 없음 |
curl:
# Trigger retranscoding with current default settings
curl -X POST https://api.xylolabs.com/api/v1/uploads/880e8400-e29b-41d4-a716-446655440000/retranscode \
-H "Authorization: Bearer $TOKEN"
8. 오디오 스트리밍¶
GET /api/v1/streams/{id}¶
트랜스코딩이 끝난 오디오를 백엔드가 직접 프록시해서 스트리밍한다. 내부 S3 엔드포인트를 브라우저에 노출하지 않고도 바로 재생할 수 있다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 업로드 ID |
응답 200 OK:
헤더:
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
다른 시설의 업로드에 접근 |
404 Not Found |
업로드가 없거나 완료된 트랜스코딩이 없음 |
curl:
# Follow redirect and save the transcoded audio to a file
curl -L https://api.xylolabs.com/api/v1/streams/880e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN" \
--output turbine_hall_transcoded.webm
9. 트랜스코딩 작업¶
GET /api/v1/transcode-jobs¶
시설의 트랜스코딩 작업 목록을 페이지네이션으로 조회한다.
인증: JWT Bearer Token
필요 역할: user 이상
쿼리 파라미터:
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
page |
integer | 1 | 페이지 번호 |
per_page |
integer | 20 | 페이지당 항목 수 (1-100) |
status |
string | -- | 필터: queued, running, completed, failed, cancelled |
응답 200 OK:
{
"items": [
{
"id": "aa0e8400-e29b-41d4-a716-446655440000",
"upload_id": "880e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"target_format": "opus_webm",
"target_codec": "libopus",
"target_container": "webm",
"target_bitrate": "128k",
"target_sample_rate": null,
"output_size_bytes": 2048000,
"output_duration_ms": 120000,
"status": "completed",
"error_message": null,
"attempts": 1,
"max_attempts": 3,
"queued_at": "2025-06-15T14:30:05Z",
"started_at": "2025-06-15T14:30:10Z",
"completed_at": "2025-06-15T14:30:25Z",
"worker_id": "worker-01"
}
],
"total": 10,
"page": 1,
"per_page": 20
}
curl:
# List completed transcoding jobs
curl "https://api.xylolabs.com/api/v1/transcode-jobs?status=completed" \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/transcode-jobs/{id}¶
트랜스코딩 작업 하나를 상세 조회한다.
인증: JWT Bearer Token
필요 역할: user 이상
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 트랜스코딩 작업 ID |
응답 200 OK: 트랜스코딩 작업 객체 (목록 항목과 동일한 스키마).
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
다른 시설의 작업에 접근 |
404 Not Found |
해당 작업이 없음 |
curl:
# Check status of a specific transcoding job
curl https://api.xylolabs.com/api/v1/transcode-jobs/aa0e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/transcode-jobs/{id}/cancel¶
대기(queued) 또는 실행 중(running)인 트랜스코딩 작업을 취소한다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 트랜스코딩 작업 ID |
응답 204 No Content
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
권한 부족 또는 다른 시설의 작업에 접근 |
404 Not Found |
해당 작업이 없음 |
curl:
# Cancel a queued transcoding job
curl -X POST https://api.xylolabs.com/api/v1/transcode-jobs/aa0e8400-e29b-41d4-a716-446655440000/cancel \
-H "Authorization: Bearer $TOKEN"
10. 태그¶
태그는 업로드를 분류하고 정리하는 레이블이다. 시설별로 관리되며 색상 코드를 지정할 수 있다.
GET /api/v1/tags¶
시설에 등록된 태그 전체를 반환한다.
인증: JWT Bearer Token
필요 역할: user 이상
응답 200 OK:
[
{
"id": "990e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "터빈 진동",
"color": "#F44336",
"created_at": "2025-06-01T09:00:00Z"
},
{
"id": "990e8400-e29b-41d4-a716-446655440001",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "해양 환경음",
"color": "#2196F3",
"created_at": "2025-06-02T10:00:00Z"
},
{
"id": "990e8400-e29b-41d4-a716-446655440002",
"facility_id": "660e8400-e29b-41d4-a716-446655440002",
"name": "조류 음성",
"color": "#4CAF50",
"created_at": "2025-06-03T11:00:00Z"
}
]
curl:
# List all tags in the facility
curl https://api.xylolabs.com/api/v1/tags \
-H "Authorization: Bearer $TOKEN"
POST /api/v1/tags¶
새 태그를 만든다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
요청 본문:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
string | O | 태그 이름 |
color |
string | X | 대시보드 표시용 hex 색상 (예: #9C27B0) |
응답 200 OK: 생성된 태그 객체.
curl:
# Create a tag for nighttime recordings
curl -X POST https://api.xylolabs.com/api/v1/tags \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"야간 녹음","color":"#9C27B0"}'
DELETE /api/v1/tags/{id}¶
태그를 삭제한다. 삭제해도 이미 업로드에 연결된 태그 관계만 끊기며 업로드 자체는 영향받지 않는다.
인증: JWT Bearer Token
필요 역할: facility_admin 이상
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 태그 ID |
응답 204 No Content
curl:
# Delete an unused tag
curl -X DELETE https://api.xylolabs.com/api/v1/tags/990e8400-e29b-41d4-a716-446655440002 \
-H "Authorization: Bearer $TOKEN"
11. 메타데이터 수집¶
메타데이터 수집(Ingest) 서브시스템은 임베디드 디바이스가 XMBP(Xylolabs Metadata Binary Protocol)로 센서 데이터를 실시간 전송하는 기능이다. 세션을 열고, 데이터를 보내고, 세션을 닫는 3단계로 동작한다.
수집 흐름¶
1. 세션 생성 (POST /ingest/sessions)
--> 세션 ID + 스트림 정의 반환
2. 데이터 전송 (반복)
|-- HTTP: POST /ingest/sessions/{id}/data (XMBP binary body)
|-- WebSocket: /ingest/ws (XMBP binary frames)
3. 세션 종료 (POST /ingest/sessions/{id}/close)
세션 라이프사이클 참고: 세션은
active->closing->closed순서로 진행한다. 세션을 명시적으로 닫지 않아도 데이터가 유실되지는 않지만,closed상태여야 export/조회 시 데이터셋이 완전하다고 보장할 수 있다. 디바이스 펌웨어에서 정상 종료 시 반드시/close를 호출한다.HTTP POST vs WebSocket, 어떤 걸 쓸까? - HTTP POST: 구현이 간단하고, Cat-M1 모뎀처럼 WebSocket을 지원하지 않는 환경에 맞는다. 배치마다 요청-응답 한 쌍이 오가므로 간헐적 전송에 좋는다. - WebSocket: 한 번 연결하면 프레임 오버헤드가 거의 없다. 연속 스트리밍(수백 Hz 이상)에서 효율이 높지만, 재연결 로직을 직접 짜야 한다. - Cat-M1 환경: 대부분의 Cat-M1 AT 명령 스택은 HTTP만 지원하므로 HTTP POST를 쓴다.
POST /api/v1/ingest/sessions¶
스트림 정의와 함께 새 수집 세션을 생성한다. device_uid에 해당하는 디바이스가 아직 없으면 자동으로 등록된다.
인증: API Key (X-Api-Key 헤더)
필요 역할: 해당 없음 (API Key로 시설 범위 지정)
요청 본문:
{
"device_uid": 1,
"name": "터빈홀 연속 모니터링 2025-06-15",
"streams": [
{
"stream_index": 0,
"name": "temperature",
"value_type": "f32",
"unit": "celsius",
"sample_rate_hz": 100.0,
"description": "Turbine bearing surface temperature"
},
{
"stream_index": 1,
"name": "vibration_rms",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 100.0,
"description": "RMS vibration of turbine shaft"
},
{
"stream_index": 2,
"name": "humidity",
"value_type": "f32",
"unit": "percent",
"sample_rate_hz": 10.0,
"description": "Ambient relative humidity"
},
{
"stream_index": 3,
"name": "pressure",
"value_type": "f32",
"unit": "hPa",
"sample_rate_hz": 10.0,
"description": "Barometric pressure"
}
],
"metadata": {
"location": "turbine_hall_east",
"firmware": "1.2.0",
"calibration_date": "2025-06-01"
}
}
CreateIngestSessionRequest 필드:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
device_uid |
u32 | O | 시설 내 고유 디바이스 번호 |
name |
string | X | 세션 이름 |
streams |
StreamDefinition[] | O | 스트림 정의 배열 |
metadata |
object | X | 세션에 첨부할 메타데이터 |
StreamDefinition 필드:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
stream_index |
u16 | O | 스트림 인덱스 (XMBP의 stream_id와 매칭) |
name |
string | O | 스트림 이름 |
value_type |
string | O | 값 타입: f32, f64, i32, i64, bool, string, bytes, f64_array, f32_array, i32_array, json, i16, i8 |
unit |
string | X | 단위 레이블 (표시용) |
sample_rate_hz |
f32 | X | 샘플링 레이트 (표시용) |
description |
string | X | 설명 |
응답 201 Created:
{
"id": "dd0e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"device_id": "cc0e8400-e29b-41d4-a716-446655440000",
"status": "active",
"started_at": "2025-06-15T10:00:00Z",
"total_samples": 0,
"total_bytes": 0,
"missed_batches": 0,
"streams": [
{
"id": "ee0e8400-e29b-41d4-a716-446655440000",
"stream_index": 0,
"name": "temperature",
"value_type": "f32",
"unit": "celsius",
"sample_rate_hz": 100.0
},
{
"id": "ee0e8400-e29b-41d4-a716-446655440001",
"stream_index": 1,
"name": "vibration_rms",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 100.0
}
]
}
curl:
# Create a session with temperature + vibration streams
curl -X POST https://api.xylolabs.com/api/v1/ingest/sessions \
-H "X-Api-Key: xk_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{
"device_uid": 1,
"name": "터빈홀 연속 모니터링 2025-06-15",
"streams": [
{
"stream_index": 0,
"name": "temperature",
"value_type": "f32",
"unit": "celsius",
"sample_rate_hz": 100
},
{
"stream_index": 1,
"name": "vibration_rms",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 100
}
],
"metadata": {"location":"turbine_hall_east","firmware":"1.2.0"}
}'
POST /api/v1/ingest/sessions/{id}/data¶
HTTP POST로 XMBP 바이너리 배치를 전송한다. 배치 구성 방법은 XMBP 프로토콜 레퍼런스를 참고한다.
인증: API Key (X-Api-Key 헤더)
필요 역할: 해당 없음 (API Key로 시설 범위 지정)
Content-Type: application/octet-stream
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 세션 ID |
요청 본문: 원시 XMBP 바이너리 배치 (XMBP 프로토콜 레퍼런스 참조).
응답 200 OK:
| 필드 | 타입 | 설명 |
|---|---|---|
accepted_samples |
u32 | 이 세션에서 지금까지 받은 총 샘플 수 |
missed_batches |
u32 | batch_seq 기준으로 감지된 누락 배치 수 |
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
400 Bad Request |
XMBP 디코딩 오류 (잘못된 magic, 손상된 페이로드 등) |
401 Unauthorized |
유효하지 않은 API 키 |
404 Not Found |
세션이 없거나 시설 불일치 |
curl:
# Send a binary XMBP batch to an active session
curl -X POST https://api.xylolabs.com/api/v1/ingest/sessions/dd0e8400-e29b-41d4-a716-446655440000/data \
-H "X-Api-Key: xk_a1b2c3d4e5f6..." \
-H "Content-Type: application/octet-stream" \
--data-binary @batch_001.bin
POST /api/v1/ingest/sessions/{id}/close¶
수집 세션을 종료한다. 서버 내부 버퍼를 플러시하고 세션 상태를 closed로 바꾼다. 종료된 세션에는 더 이상 데이터를 보낼 수 없다.
인증: API Key (X-Api-Key 헤더)
필요 역할: 해당 없음 (API Key로 시설 범위 지정)
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 세션 ID |
응답 200 OK: 최종 세션 상태.
{
"id": "dd0e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"device_id": "cc0e8400-e29b-41d4-a716-446655440000",
"status": "closed",
"started_at": "2025-06-15T10:00:00Z",
"total_samples": 1200000,
"total_bytes": 9600000,
"missed_batches": 2,
"streams": [
{
"id": "ee0e8400-e29b-41d4-a716-446655440000",
"stream_index": 0,
"name": "temperature",
"value_type": "f32",
"unit": "celsius",
"sample_rate_hz": 100.0
}
]
}
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
세션이 다른 시설에 속함 |
404 Not Found |
해당 세션이 없음 |
curl:
# Close a session and flush all buffered data
curl -X POST https://api.xylolabs.com/api/v1/ingest/sessions/dd0e8400-e29b-41d4-a716-446655440000/close \
-H "X-Api-Key: xk_a1b2c3d4e5f6..."
GET /api/v1/ingest/ws¶
실시간 바이너리 데이터 스트리밍을 위한 WebSocket 업그레이드 엔드포인트이다.
인증: X-Api-Key 헤더 필수
필요 역할: 해당 없음
쿼리 파라미터:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
session_id |
UUID | O | 대상 세션 ID |
쿼리 파라미터에 API 키를 넣으면 프록시, CDN, 브라우저 로그에 남을 수 있으므로 지원하지 않는다.
연결:
GET /api/v1/ingest/ws?session_id=dd0e8400-...
Connection: Upgrade
Upgrade: websocket
X-Api-Key: xk_a1b2...
프로토콜:
| 방향 | 프레임 타입 | 내용 |
|---|---|---|
| 클라이언트 --> 서버 | Binary | XMBP 배치 |
| 서버 --> 클라이언트 | Binary | ACK: 4바이트 u32 big-endian (accepted_samples) |
| 서버 --> 클라이언트 | Text | 오류 메시지 (이후 연결 종료) |
동작 사양: - 유휴 타임아웃: 60초간 메시지가 없으면 서버가 연결을 끊는다. - Ping/Pong: 서버는 Ping 프레임에 자동으로 Pong으로 응답한다. - 오류: XMBP 디코딩 실패 시 Text 프레임으로 오류를 보내고 연결을 끊을 수 있다. - 최대 프레임 크기: 64 KB - 최대 메시지 크기: 256 KB
ACK 처리 (C pseudocode):
// Read 4-byte big-endian ACK from server
uint8_t ack_buf[4];
ws_recv(ack_buf, 4);
uint32_t accepted = (ack_buf[0] << 24) | (ack_buf[1] << 16)
| (ack_buf[2] << 8) | ack_buf[3];
printf("Server accepted %u total samples\n", accepted);
WebSocket vs HTTP 비교:
| 항목 | HTTP POST | WebSocket |
|---|---|---|
| 연결 오버헤드 | 배치마다 새 연결 (또는 keep-alive) | 한 번 연결 |
| 헤더 오버헤드 | 요청당 ~200바이트 | 프레임당 2-14바이트 |
| ACK 형식 | JSON 응답 | 4바이트 바이너리 |
| 적합한 용도 | 간헐적 전송, Cat-M1 | 연속 고속 스트리밍 |
| 재연결 | 불필요 | 직접 구현 필요 |
12. 메타데이터 조회¶
수집이 끝난 센서 데이터를 조회한다. 모든 엔드포인트에 JWT 인증이 필요하다.
GET /api/v1/metadata/sessions¶
시설의 수집 세션 목록을 조회한다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
쿼리 파라미터:
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
page |
integer | 1 | 페이지 번호 |
per_page |
integer | 20 | 페이지당 항목 수 (1-100) |
status |
string | -- | 필터: active, closing, closed, error |
device_id |
UUID | -- | 특정 디바이스의 세션만 표시 |
응답 200 OK:
{
"items": [
{
"id": "dd0e8400-e29b-41d4-a716-446655440000",
"facility_id": "660e8400-e29b-41d4-a716-446655440000",
"device_id": "cc0e8400-e29b-41d4-a716-446655440000",
"status": "closed",
"started_at": "2025-06-15T10:00:00Z",
"total_samples": 1200000,
"total_bytes": 9600000,
"missed_batches": 2,
"streams": [
{
"id": "ee0e8400-e29b-41d4-a716-446655440000",
"stream_index": 0,
"name": "temperature",
"value_type": "f32",
"unit": "celsius",
"sample_rate_hz": 100.0
},
{
"id": "ee0e8400-e29b-41d4-a716-446655440001",
"stream_index": 1,
"name": "vibration_rms",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 100.0
}
]
}
],
"total": 5,
"page": 1,
"per_page": 20
}
curl:
# List closed sessions for a specific device
curl "https://api.xylolabs.com/api/v1/metadata/sessions?status=closed&device_id=cc0e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/metadata/sessions/{id}¶
스트림 정의를 포함한 세션 상세 정보를 반환한다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 세션 ID |
응답 200 OK: 세션 객체 (목록 항목과 동일한 스키마).
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
다른 시설의 세션에 접근 |
404 Not Found |
해당 세션이 없음 |
curl:
# Get full session details including stream definitions
curl https://api.xylolabs.com/api/v1/metadata/sessions/dd0e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/metadata/sessions/{id}/streams/{stream_id}/data¶
특정 스트림의 데이터 샘플을 시간 범위와 다운샘플링 옵션으로 조회한다. 많은 양의 데이터를 브라우저 차트에 표시할 때 downsample 파라미터로 서버 쪽에서 포인트 수를 줄일 수 있다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 세션 ID |
stream_id |
UUID | 스트림 ID |
쿼리 파라미터:
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
start_us |
i64 | 0 | 시작 타임스탬프 (마이크로초) |
end_us |
i64 | MAX | 종료 타임스탬프 (마이크로초) |
downsample |
u32 | -- | 대상 포인트 수 (서버 측 다운샘플링) |
응답 200 OK:
{
"stream_id": "ee0e8400-e29b-41d4-a716-446655440000",
"stream_name": "temperature",
"value_type": "f32",
"sample_count": 1000,
"timestamps_us": [1718442000000000, 1718442010000, 1718442020000],
"values": [72.3, 72.5, 72.1]
}
curl:
# Get temperature data downsampled to 1000 points for charting
curl "https://api.xylolabs.com/api/v1/metadata/sessions/dd0e8400-e29b-41d4-a716-446655440000/streams/ee0e8400-e29b-41d4-a716-446655440000/data?start_us=0&downsample=1000" \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/metadata/sessions/{id}/export¶
세션의 전체 데이터를 CSV 또는 JSON 파일로 내보낸다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 세션 ID |
쿼리 파라미터:
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
format |
string | csv |
내보내기 형식: csv 또는 json |
응답 200 OK:
CSV 형식:
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="session_dd0e8400-....csv"
timestamp_us,temperature,vibration_rms,humidity,pressure
1718442000000000,72.3,0.42,45.2,1013.25
1718442010000,72.5,0.38,45.1,1013.24
1718442020000,72.1,0.55,45.3,1013.26
JSON 형식:
{
"session_id": "dd0e8400-e29b-41d4-a716-446655440000",
"streams": [
{
"stream_id": "ee0e8400-e29b-41d4-a716-446655440000",
"stream_name": "temperature",
"value_type": "f32",
"sample_count": 1200000,
"timestamps_us": [1718442000000000, 1718442010000],
"values": [72.3, 72.5]
},
{
"stream_id": "ee0e8400-e29b-41d4-a716-446655440001",
"stream_name": "vibration_rms",
"value_type": "f32",
"sample_count": 1200000,
"timestamps_us": [1718442000000000, 1718442010000],
"values": [0.42, 0.38]
}
]
}
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
400 Bad Request |
지원하지 않는 내보내기 형식 |
403 Forbidden |
다른 시설의 세션에 접근 |
404 Not Found |
해당 세션이 없음 |
curl:
# Export session data as CSV for offline analysis
curl "https://api.xylolabs.com/api/v1/metadata/sessions/dd0e8400-e29b-41d4-a716-446655440000/export?format=csv" \
-H "Authorization: Bearer $TOKEN" \
--output turbine_monitoring_2025-06-15.csv
GET /api/v1/metadata/sessions/{id}/live¶
Server-Sent Events(SSE)로 활성 수집 세션의 실시간 데이터를 구독한다. 대시보드에서 라이브 차트를 그릴 때 쓴다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
id |
UUID | 세션 ID (active 상태여야 함) |
응답: SSE 스트림
event: sample
data: {"stream_index":0,"timestamp_us":1718442000000000,"value":72.3}
event: sample
data: {"stream_index":1,"timestamp_us":1718442000000000,"value":0.42}
event: lag
data: {"skipped":10}
이벤트:
| 이벤트 | 설명 |
|---|---|
sample |
디바이스에서 받은 새 데이터 포인트 |
lag |
클라이언트 소비 속도가 느려서 일부 이벤트를 건너뜀 |
curl:
# Subscribe to live sensor data (Ctrl+C to stop)
curl -N "https://api.xylolabs.com/api/v1/metadata/sessions/dd0e8400-e29b-41d4-a716-446655440000/live" \
-H "Authorization: Bearer $TOKEN"
13. 시스템 설정¶
슈퍼 관리자가 관리하는 런타임 설정이다. 값은 DB에 저장되고 메모리에 캐시되어 변경 즉시 반영된다.
GET /api/v1/config¶
카테고리별로 묶인 전체 설정 항목을 반환한다.
인증: JWT Bearer Token
필요 역할: super_admin
응답 200 OK:
{
"categories": [
{
"category": "transcode",
"entries": [
{
"key": "transcode.default_format",
"value": "opus_webm",
"category": "transcode",
"value_type": "string",
"description": "Default transcoding output format",
"updated_at": "2025-06-15T10:00:00Z"
},
{
"key": "transcode.default_bitrate",
"value": "128k",
"category": "transcode",
"value_type": "string",
"description": "Default transcoding bitrate",
"updated_at": "2025-06-15T10:00:00Z"
}
]
}
]
}
curl:
# Get all system configuration grouped by category
curl https://api.xylolabs.com/api/v1/config \
-H "Authorization: Bearer $TOKEN"
GET /api/v1/config/{key}¶
설정 항목 하나를 조회한다.
인증: JWT Bearer Token
필요 역할: super_admin
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
key |
string | 설정 키 (예: transcode.default_format) |
응답 200 OK:
{
"key": "transcode.default_format",
"value": "opus_webm",
"category": "transcode",
"value_type": "string",
"description": "Default transcoding output format",
"updated_at": "2025-06-15T10:00:00Z"
}
오류 케이스:
| 상태 코드 | 조건 |
|---|---|
403 Forbidden |
슈퍼 관리자가 아닌 경우 |
404 Not Found |
해당 키가 없음 |
curl:
# Read a single config entry
curl https://api.xylolabs.com/api/v1/config/transcode.default_format \
-H "Authorization: Bearer $TOKEN"
PUT /api/v1/config/{key}¶
설정 값을 변경한다.
인증: JWT Bearer Token
필요 역할: super_admin
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
key |
string | 설정 키 |
요청 본문:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
value |
any (JSON) | O | 새 값 (value_type에 맞는 타입이어야 함) |
응답 200 OK: 수정된 설정 항목 객체.
curl:
# Switch default transcoding to AAC/MP4
curl -X PUT https://api.xylolabs.com/api/v1/config/transcode.default_format \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value":"aac_mp4"}'
POST /api/v1/config/reset/{key}¶
설정 값을 기본값으로 되돌립니다.
인증: JWT Bearer Token
필요 역할: super_admin
경로 파라미터:
| 파라미터 | 타입 | 설명 |
|---|---|---|
key |
string | 설정 키 |
응답 200 OK: 기본값으로 초기화된 설정 항목 객체.
curl:
# Reset transcoding format back to default (opus_webm)
curl -X POST https://api.xylolabs.com/api/v1/config/reset/transcode.default_format \
-H "Authorization: Bearer $TOKEN"
14. 대시보드 통계¶
GET /api/v1/stats/overview¶
대시보드에 표시할 통계 요약을 반환한다. 역할에 따라 응답 필드가 달라진다.
인증: JWT Bearer Token 필요 역할: 인증된 모든 사용자
응답 200 OK:
시설 관리자 / 일반 사용자:
슈퍼 관리자 (시설 수 포함):
curl:
# Get dashboard overview stats
curl https://api.xylolabs.com/api/v1/stats/overview \
-H "Authorization: Bearer $TOKEN"
15. 헬스 체크¶
GET /api/health¶
서버가 살아 있는지 확인하는 기본 헬스 체크이다. 서버가 실행 중이면 항상 200을 반환한다. 로드 밸런서의 liveness probe로 쓴다.
인증: 없음 필요 역할: 없음
응답 200 OK:
curl:
GET /api/health/ready¶
PostgreSQL과 S3 연결 상태까지 확인하는 readiness 체크이다.
인증: 없음 필요 역할: 없음
응답 200 OK:
S3에 접근할 수 없는 경우:
curl:
GET/POST /api/v1/ping¶
API 키 유효성 검증 및 연결 테스트. X-Api-Key 헤더가 유효한지 확인하고 키 이름과 연결된 시설 정보를 반환한다. 선택적으로 message 필드를 보내면 그대로 돌려준다.
MCU 펌웨어에서 인제스트 세션 시작 전에 API 키가 작동하는지 확인할 때 사용한다.
인증: API 키 (X-Api-Key 헤더)
요청 본문 (선택, POST만 해당):
응답 200 OK:
{
"pong": true,
"message": "hello from pico",
"api_key_name": "production-sensor-01",
"facility_id": "b666b8c3-bbc2-4675-a3ac-8b6a1e6dad9f"
}
본문을 보내지 않으면 message는 "pong"이 기본값이다.
오류:
- 401 — API 키 누락 또는 유효하지 않음
curl:
# GET — 간단한 키 검증
curl -H "X-Api-Key: xk_your_key_here" https://api.xylolabs.com/api/v1/ping
# POST — 에코 메시지 포함
curl -X POST -H "X-Api-Key: xk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"message":"test"}' \
https://api.xylolabs.com/api/v1/ping
16. XMBP 프로토콜 레퍼런스¶
XMBP(Xylolabs Metadata Binary Protocol)는 Cat-M1 같은 저대역폭 환경에서 센서 데이터를 적은 바이트로 전송하기 위한 바이너리 프로토콜이다. 모든 숫자는 빅엔디안 바이트 순서를 사용한다.
Batch Envelope¶
HTTP POST 본문 또는 WebSocket 바이너리 프레임 하나에 대응하는 최상위 구조이다.
+--------+------+--------------------------------------+
| Offset | Size | Field |
+--------+------+--------------------------------------+
| 0 | 4 | magic (0x584D4250 = "XMBP") |
| 4 | 1 | version (1) |
| 5 | 1 | flags |
| 6 | 2 | batch_seq (u16) |
| 8 | 4 | device_id (u32, conditional) |
| 8/12 | 2 | stream_count (u16) |
| 10/14 | ... | StreamBlock[stream_count] |
+--------+------+--------------------------------------+
Flags:
| 비트 | 이름 | 설명 |
|---|---|---|
| 0 (0x01) | HAS_DEVICE_ID |
설정 시 offset 8에 device_id 필드(4바이트) 포함 |
| 1 (0x02) | ZSTD_COMPRESSED |
예약 (향후 zstd 압축용) |
| 2-7 | -- | 예약 |
HAS_DEVICE_ID가 설정되면 stream_count는 offset 12에서, 그렇지 않으면 offset 8에서 시작한다.
StreamBlock¶
단일 스트림(채널)의 데이터 블록이다.
+--------+------+--------------------------------------+
| Offset | Size | Field |
+--------+------+--------------------------------------+
| 0 | 2 | stream_id (u16) |
| 2 | 1 | value_type (u8) |
| 3 | 2 | sample_count (u16) |
| 5 | ... | Sample[sample_count] |
+--------+------+--------------------------------------+
Sample 형식¶
고정 크기 타입 (f32, f64, i32, i64, bool):
+--------+------+--------------------------------------+
| Offset | Size | Field |
+--------+------+--------------------------------------+
| 0 | 8 | timestamp_us (u64, microseconds) |
| 8 | N | value (fixed size per type) |
+--------+------+--------------------------------------+
가변 크기 타입 (string, bytes, arrays, json):
+--------+------+--------------------------------------+
| Offset | Size | Field |
+--------+------+--------------------------------------+
| 0 | 8 | timestamp_us (u64, microseconds) |
| 8 | 2 | length (u16, bytes or count) |
| 10 | ... | data (variable) |
+--------+------+--------------------------------------+
Value Type 태그¶
| 태그 | 타입 | 크기 | 설명 |
|---|---|---|---|
| 0 | f64 |
8바이트 | 64-bit IEEE 754 float |
| 1 | f32 |
4바이트 | 32-bit IEEE 754 float |
| 2 | i64 |
8바이트 | 64-bit signed integer |
| 3 | i32 |
4바이트 | 32-bit signed integer |
| 4 | bool |
1바이트 | 0 = false, nonzero = true |
| 5 | string |
가변 | u16 length + UTF-8 bytes |
| 6 | bytes |
가변 | u16 length + raw bytes |
| 7 | f64_array |
가변 | u16 element count + f64[] |
| 8 | f32_array |
가변 | u16 element count + f32[] |
| 9 | i32_array |
가변 | u16 element count + i32[] |
| 10 | json |
가변 | u32 length + UTF-8 JSON string |
| 11 | i16 |
2바이트 | 16비트 부호 있는 정수, big-endian |
| 12 | i8 |
1바이트 | 8비트 부호 있는 정수 (u8 cast to i8) |
XMBP 배치 구성 단계별 워크스루¶
f32 온도 데이터 3샘플을 담은 단일 스트림 배치를 직접 만들어 보겠는다.
Step 1: 헤더 작성
Offset Hex Field
------ --------------- ---------------------------
0x00 58 4D 42 50 magic = "XMBP"
0x04 01 version = 1
0x05 01 flags = HAS_DEVICE_ID
0x06 00 00 batch_seq = 0
0x08 00 00 00 01 device_id = 1
0x0C 00 01 stream_count = 1
Step 2: StreamBlock 작성
Offset Hex Field
------ --------------- ---------------------------
0x0E 00 00 stream_id = 0
0x10 01 value_type = 1 (f32)
0x11 00 03 sample_count = 3
Step 3: Sample 데이터 작성
Offset Hex Field
------ --------------------------- ---------------------------
0x13 00 00 06 39 8C 92 58 00 timestamp = 1718442000000000 us
0x1B 42 90 CC CD value = 72.4 (f32 big-endian)
0x1F 00 00 06 39 8C 92 60 70 timestamp = 1718442002160 us
0x27 42 91 33 33 value = 72.6 (f32 big-endian)
0x2B 00 00 06 39 8C 92 68 E0 timestamp = 1718442004320 us
0x33 42 90 66 66 value = 72.2 (f32 big-endian)
전체 배치 크기: 0x37 = 55바이트
Cat-M1 최적화 참고: Cat-M1의 실측 uplink는 약 37KB/s이다. 위 예제처럼 작은 배치는 1ms 이내에 전송할 수 있지만, 실전에서는 배치당 250샘플 정도로 묶어서 HTTP 오버헤드 대비 페이로드 비율을 높여야 한다. 아래 대역폭 예산 섹션에서 자세히 다룬다.
배치 크기 계산¶
고정 크기 타입이면 배치 크기를 미리 계산할 수 있다.
Header size:
base = 4 (magic) + 1 (version) + 1 (flags) + 2 (batch_seq) + 2 (stream_count) = 10
with device_id: +4 = 14
Stream block size (fixed-size types):
block_header = 2 (stream_id) + 1 (value_type) + 2 (sample_count) = 5
sample_size = 8 (timestamp) + value_size
Total batch size:
header_size + SUM(5 + sample_count * (8 + value_size)) for each stream
예시: 4채널 f32, 250 samples/batch, device_id 포함
14 + 4 * (5 + 250 * (8 + 4))
= 14 + 4 * (5 + 250 * 12)
= 14 + 4 * 3005
= 14 + 12020
= 12,034 bytes (~12KB)
이 크기는 Cat-M1으로 약 325ms면 전송할 수 있어 실시간 스트리밍에 쓸 수 있다.
17. RBAC 레퍼런스¶
역할¶
| 역할 | 레벨 | 설명 |
|---|---|---|
super_admin |
3 | 모든 시설에 대한 전체 접근 권한 |
facility_admin |
2 | 소속 시설의 사용자, 키, 디바이스, 데이터 관리 |
user |
1 | 소속 시설 데이터 읽기 전용 |
역할 확인은 >= 비교를 사용한다. super_admin은 어떤 역할 검사도 통과한다.
권한 매트릭스¶
| 엔드포인트 | super_admin |
facility_admin |
user |
|---|---|---|---|
| 시설 CRUD | O | -- | -- |
| 사용자 CRUD | O | 자기 시설만 | -- |
| API 키 CRUD | O | 자기 시설만 | -- |
| 디바이스 조회 | O | O | O |
| 디바이스 생성/수정 | O | 자기 시설만 | -- |
| 업로드 조회 | O | O | O |
| 업로드 생성 (API Key) | API Key 사용 | API Key 사용 | API Key 사용 |
| 업로드 수정/삭제 | O | O | O |
| 업로드 재트랜스코딩 | O | O | O |
| 스트리밍 조회 | O | O | O |
| 트랜스코딩 작업 조회 | O | O | O |
| 트랜스코딩 작업 취소 | O | 자기 시설만 | -- |
| 태그 조회 | O | O | O |
| 태그 생성/삭제 | O | 자기 시설만 | -- |
| 수집 (API Key) | API Key 사용 | API Key 사용 | API Key 사용 |
| 메타데이터 조회 | O | O | O |
| 시스템 설정 CRUD | O | -- | -- |
| 통계 개요 | O | O | O |
시설 격리¶
- 슈퍼 관리자가 아닌 사용자는 자기가 배정된 시설의 데이터만 볼 수 있다.
- API 키는 하나의 시설에 묶여 있다.
- 다른 시설의 리소스에 접근하면
403 Forbidden이 반환된다.
18. 오류 응답¶
모든 오류는 같은 JSON 형식을 따른다.
HTTP 상태 코드¶
| 상태 코드 | 의미 | 주요 원인 |
|---|---|---|
400 Bad Request |
잘못된 요청 | 필드 누락, 잘못된 JSON, XMBP 디코딩 오류 |
401 Unauthorized |
인증 실패 | JWT/API Key가 없거나 유효하지 않음, 토큰 만료 |
403 Forbidden |
권한 부족 | 역할 미달, 다른 시설 접근 시도 |
404 Not Found |
리소스 없음 | 잘못된 ID, 삭제된 리소스 |
409 Conflict |
리소스 충돌 | 중복 이메일, 중복 slug, 중복 device_uid |
500 Internal Server Error |
서버 오류 | DB 오류, S3 오류, 예상치 못한 실패 |
오류 응답 예시¶
401 (인증 실패):
403 (권한 부족):
404 (리소스 없음):
409 (중복 충돌):
19. 데이터 모델¶
Facility (시설)¶
{
"id": "UUID",
"name": "string",
"slug": "string",
"description": "string | null",
"is_active": "boolean",
"created_at": "ISO 8601",
"updated_at": "ISO 8601"
}
User (사용자)¶
{
"id": "UUID",
"facility_id": "UUID | null",
"email": "string",
"display_name": "string",
"role": "super_admin | facility_admin | user",
"is_active": "boolean",
"last_login_at": "ISO 8601 | null",
"created_at": "ISO 8601",
"updated_at": "ISO 8601"
}
ApiKey (API 키)¶
{
"id": "UUID",
"facility_id": "UUID",
"key_prefix": "string",
"name": "string",
"scopes": ["string"],
"is_active": "boolean",
"expires_at": "ISO 8601 | null",
"last_used_at": "ISO 8601 | null",
"created_by": "UUID | null",
"created_at": "ISO 8601"
}
Device (디바이스)¶
{
"id": "UUID",
"facility_id": "UUID",
"device_uid": "integer",
"name": "string",
"hardware_version": "string | null",
"firmware_version": "string | null",
"is_active": "boolean",
"last_seen_at": "ISO 8601 | null",
"created_at": "ISO 8601"
}
Upload (업로드)¶
{
"id": "UUID",
"facility_id": "UUID",
"api_key_id": "UUID | null",
"uploaded_by": "UUID | null",
"original_filename": "string",
"original_format": "string",
"original_size_bytes": "integer",
"duration_ms": "integer | null",
"sample_rate_hz": "integer | null",
"channels": "integer | null",
"bit_depth": "integer | null",
"codec": "string | null",
"metadata": "object",
"status": "pending | processing | completed | failed",
"tags": ["Tag"],
"created_at": "ISO 8601",
"updated_at": "ISO 8601"
}
TranscodeJob (트랜스코딩 작업)¶
{
"id": "UUID",
"upload_id": "UUID",
"facility_id": "UUID",
"target_format": "string",
"target_codec": "string",
"target_container": "string",
"target_bitrate": "string | null",
"target_sample_rate": "integer | null",
"output_size_bytes": "integer | null",
"output_duration_ms": "integer | null",
"status": "queued | running | completed | failed | cancelled",
"error_message": "string | null",
"attempts": "integer",
"max_attempts": "integer",
"queued_at": "ISO 8601",
"started_at": "ISO 8601 | null",
"completed_at": "ISO 8601 | null",
"worker_id": "string | null"
}
Tag (태그)¶
{
"id": "UUID",
"facility_id": "UUID",
"name": "string",
"color": "string | null",
"created_at": "ISO 8601"
}
IngestSession (수집 세션)¶
{
"id": "UUID",
"facility_id": "UUID",
"device_id": "UUID",
"status": "active | closing | closed | error",
"started_at": "ISO 8601",
"total_samples": "integer",
"total_bytes": "integer",
"missed_batches": "integer",
"streams": ["MetadataStream"]
}
MetadataStream (메타데이터 스트림)¶
{
"id": "UUID",
"stream_index": "integer",
"name": "string",
"value_type": "f32 | f64 | i32 | i64 | bool | string | bytes | f64_array | f32_array | i32_array | json | i16 | i8",
"unit": "string | null",
"sample_rate_hz": "float | null"
}
ConfigEntry (설정 항목)¶
{
"key": "string",
"value": "any (JSON)",
"category": "string",
"value_type": "string",
"description": "string | null",
"updated_at": "ISO 8601"
}
20. 페이지네이션¶
페이지네이션을 지원하는 엔드포인트는 동일한 쿼리 파라미터와 응답 형식을 쓴다.
쿼리 파라미터¶
| 파라미터 | 타입 | 기본값 | 범위 | 설명 |
|---|---|---|---|---|
page |
integer | 1 | >= 1 | 페이지 번호 (1부터 시작) |
per_page |
integer | 20 | 1-100 | 페이지당 항목 수 |
응답 형식¶
| 필드 | 타입 | 설명 |
|---|---|---|
items |
array | 현재 페이지의 데이터 |
total |
integer | 전체 항목 수 |
page |
integer | 현재 페이지 번호 |
per_page |
integer | 페이지당 항목 수 |
페이지네이션 지원 엔드포인트¶
GET /api/v1/uploadsGET /api/v1/transcode-jobsGET /api/v1/metadata/sessions
21. 워크플로 예제¶
전체 설정 흐름¶
시설 생성부터 오디오 업로드, 조회까지의 전체 과정이다.
#!/usr/bin/env bash
# End-to-end workflow: facility setup -> audio upload -> verification
# Step 1: Log in as super admin
TOKEN=$(curl -s -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "minseok.jeon@xylolabs.com",
"password": "admin-password-here"
}' | jq -r '.access_token')
echo "Logged in, token starts with: ${TOKEN:0:20}..."
# Step 2: Create a facility for Perigee Okcheon plant
FACILITY_ID=$(curl -s -X POST https://api.xylolabs.com/api/v1/facilities \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "페리지에어로스페이스 미래기술연구소",
"slug": "perigee-futuretech",
"description": "미래기술연구소 센서 데이터 수집"
}' | jq -r '.id')
echo "Created facility: $FACILITY_ID"
# Step 3: Create a facility admin for the new site
USER_ID=$(curl -s -X POST https://api.xylolabs.com/api/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"minseok.jeon@xylolabs.com\",
\"password\": \"Perigee#Okcheon2025\",
\"display_name\": \"전민석\",
\"role\": \"facility_admin\",
\"facility_id\": \"$FACILITY_ID\"
}" | jq -r '.id')
echo "Created facility admin: $USER_ID"
# Step 4: Log in as the facility admin
FA_TOKEN=$(curl -s -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "minseok.jeon@xylolabs.com",
"password": "Perigee#Okcheon2025"
}' | jq -r '.access_token')
# Step 5: Create an API key for the underwater hydrophone
API_KEY=$(curl -s -X POST https://api.xylolabs.com/api/v1/api-keys \
-H "Authorization: Bearer $FA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "수중 하이드로폰 #3",
"scopes": ["upload","ingest"],
"expires_at": "2026-06-01T00:00:00Z"
}' | jq -r '.key')
echo "API key created (save this!): ${API_KEY:0:12}..."
# Step 6: Upload an audio recording via the API key
curl -X POST https://api.xylolabs.com/api/v1/uploads \
-H "X-Api-Key: $API_KEY" \
-F "file=@hydrophone_reef_sample.flac" \
-F 'metadata={"location":"reef_zone_alpha","depth_m":12.5,"sample_rate":96000}'
echo ""
# Step 7: Verify the upload appears in the listing
echo "Recent uploads:"
curl -s "https://api.xylolabs.com/api/v1/uploads?page=1&per_page=5" \
-H "Authorization: Bearer $FA_TOKEN" | jq '.items[] | {id, original_filename, status}'
디바이스 메타데이터 스트리밍 흐름¶
세션 생성 -> XMBP 데이터 전송 -> 세션 종료 -> 데이터 조회 및 내보내기의 전체 과정이다.
#!/usr/bin/env bash
# Metadata streaming workflow: session lifecycle + data export
# Step 1: Create a session with multiple sensor streams
SESSION_ID=$(curl -s -X POST https://api.xylolabs.com/api/v1/ingest/sessions \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device_uid": 1,
"name": "터빈홀 연속 모니터링 2025-06-15",
"streams": [
{
"stream_index": 0,
"name": "temperature",
"value_type": "f32",
"unit": "celsius",
"sample_rate_hz": 100,
"description": "Turbine bearing surface temperature"
},
{
"stream_index": 1,
"name": "vibration_rms",
"value_type": "f32",
"unit": "g",
"sample_rate_hz": 100,
"description": "RMS vibration of turbine shaft"
}
],
"metadata": {"location":"turbine_hall_east","firmware":"1.2.0"}
}' | jq -r '.id')
echo "Session created: $SESSION_ID"
# Step 2: Send XMBP binary batches (repeat as needed)
# In production, your firmware builds these in a loop
curl -X POST "https://api.xylolabs.com/api/v1/ingest/sessions/$SESSION_ID/data" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @batch_001.bin
echo "Batch 1 sent"
curl -X POST "https://api.xylolabs.com/api/v1/ingest/sessions/$SESSION_ID/data" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @batch_002.bin
echo "Batch 2 sent"
# Step 3: Close the session when monitoring is done
echo "Closing session..."
curl -s -X POST "https://api.xylolabs.com/api/v1/ingest/sessions/$SESSION_ID/close" \
-H "X-Api-Key: $API_KEY" | jq '{status, total_samples, total_bytes, missed_batches}'
# Step 4: Review the session from the dashboard
echo "Session details:"
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions/$SESSION_ID" \
-H "Authorization: Bearer $TOKEN" | jq '.'
# Step 5: Export data as CSV for offline analysis
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions/$SESSION_ID/export?format=csv" \
-H "Authorization: Bearer $TOKEN" \
--output "turbine_monitoring_$(date +%Y%m%d).csv"
echo "Exported to turbine_monitoring_$(date +%Y%m%d).csv"
관리자 설정 흐름¶
시스템 설정을 확인하고 변경하는 과정이다.
#!/usr/bin/env bash
# Admin config workflow: inspect, modify, and reset settings
# Step 1: Log in as super admin
TOKEN=$(curl -s -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "minseok.jeon@xylolabs.com",
"password": "admin-password-here"
}' | jq -r '.access_token')
# Step 2: View all config grouped by category
echo "Current configuration:"
curl -s https://api.xylolabs.com/api/v1/config \
-H "Authorization: Bearer $TOKEN" | jq '.categories[] | {category, entries: [.entries[] | {key, value}]}'
# Step 3: Switch default transcoding to AAC/MP4
echo "Changing default format to AAC/MP4..."
curl -s -X PUT https://api.xylolabs.com/api/v1/config/transcode.default_format \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value":"aac_mp4"}' | jq '{key, value}'
# Step 4: Reset back to default if needed
echo "Resetting to default..."
curl -s -X POST https://api.xylolabs.com/api/v1/config/reset/transcode.default_format \
-H "Authorization: Bearer $TOKEN" | jq '{key, value}'
# Step 5: Check dashboard stats
echo "System overview:"
curl -s https://api.xylolabs.com/api/v1/stats/overview \
-H "Authorization: Bearer $TOKEN" | jq '.'
Cat-M1 디바이스 현장 배포 워크플로¶
Cat-M1 모뎀이 장착된 현장 디바이스의 최초 배포 과정이다. 관리자가 대시보드에서 시설과 키를 준비하고, 디바이스 펌웨어가 AT 명령으로 세션을 열어 데이터를 보낸다.
#!/usr/bin/env bash
# Cat-M1 field deployment: end-to-end from provisioning to data verification
# === ADMIN SIDE (laptop/dashboard) ===
# Step 1: Log in
TOKEN=$(curl -s -X POST https://api.xylolabs.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "minseok.jeon@xylolabs.com",
"password": "admin-password-here"
}' | jq -r '.access_token')
# Step 2: Create a field facility for Jeju wildlife monitoring
FACILITY_ID=$(curl -s -X POST https://api.xylolabs.com/api/v1/facilities \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "페리지에어로스페이스 옥천공장",
"slug": "perigee-okcheon",
"description": "옥천 생산공장 음향 모니터링"
}' | jq -r '.id')
# Step 3: Create an API key for the Cat-M1 device
API_KEY=$(curl -s -X POST https://api.xylolabs.com/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "조류 음성 레코더 (nRF9160)",
"scopes": ["upload","ingest"]
}' | jq -r '.key')
echo "Flash this API key into device firmware: ${API_KEY:0:12}..."
# Step 4: Pre-register the device
curl -s -X POST https://api.xylolabs.com/api/v1/devices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"device_uid": 1,
"name": "조류 음성 레코더",
"hardware_version": "nrf9160",
"firmware_version": "0.9.0"
}' | jq '{id, device_uid, name}'
# === DEVICE SIDE (firmware behavior, simulated with curl) ===
# Step 5: Device creates a session after boot
SESSION_ID=$(curl -s -X POST https://api.xylolabs.com/api/v1/ingest/sessions \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device_uid": 1,
"name": "dawn-chorus-recording-2025-06-16",
"streams": [
{
"stream_index": 0,
"name": "audio_level_rms",
"value_type": "f32",
"unit": "dBFS",
"sample_rate_hz": 10,
"description": "RMS audio level from MEMS microphone"
},
{
"stream_index": 1,
"name": "battery_voltage",
"value_type": "f32",
"unit": "V",
"sample_rate_hz": 0.1,
"description": "LiPo battery voltage"
}
],
"metadata": {"gps_lat":33.3617,"gps_lon":126.5292,"altitude_m":850}
}' | jq -r '.id')
echo "Device session started: $SESSION_ID"
# Step 6: Device sends XMBP batches over Cat-M1 (every 2.5s for 100Hz streams)
for i in 1 2 3; do
curl -s -X POST "https://api.xylolabs.com/api/v1/ingest/sessions/$SESSION_ID/data" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @batch_00${i}.bin | jq -c '{accepted_samples, missed_batches}'
done
# Step 7: Device closes session before entering sleep
curl -s -X POST "https://api.xylolabs.com/api/v1/ingest/sessions/$SESSION_ID/close" \
-H "X-Api-Key: $API_KEY" | jq '{status, total_samples, missed_batches}'
echo "Device entering PSM sleep mode..."
# === ADMIN SIDE (later, verifying collected data) ===
# Step 8: Check that data arrived
echo "Verifying data collection:"
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions?device_id=$(
curl -s https://api.xylolabs.com/api/v1/devices \
-H "Authorization: Bearer $TOKEN" | jq -r '.[0].id'
)" -H "Authorization: Bearer $TOKEN" | jq '.items[] | {id, status, total_samples, started_at}'
# Step 9: Export for analysis
curl -s "https://api.xylolabs.com/api/v1/metadata/sessions/$SESSION_ID/export?format=csv" \
-H "Authorization: Bearer $TOKEN" \
--output "jeju_dawn_chorus_$(date +%Y%m%d).csv"
echo "Data exported for ornithology analysis"
22. LTE Cat-M1 통합 가이드¶
Cat-M1(LTE-M)은 저전력 광역 통신(LPWAN) 기술로, 배터리로 구동되는 현장 센서에 맞는다. 이 섹션에서는 Cat-M1 모뎀을 Xylolabs API와 연동하는 방법을 다룹니다.
지원 모뎀¶
아래 모뎀들은 UART AT 명령으로 HTTP POST를 지원하며 Xylolabs API와 호환된다.
| 모뎀 | 인터페이스 | 특징 |
|---|---|---|
| Quectel BG770A | UART AT 명령 | Cat-M1/NB-IoT, 우수한 전력 관리. |
| u-blox SARA-R410M / R412M | UART AT 명령 | PSM/eDRX 지원이 좋아 배터리 수명 최적화에 유리. |
| Nordic nRF9160 | 통합 SoC (AT 또는 네이티브 SDK) | MCU + 모뎀 원칩. Zephyr SDK로 네이티브 개발 가능. |
| Sierra Wireless HL7800 / HL7812 | UART AT 명령 | 주요 통신사 인증 완료. 상용 배포에 맞음. |
| Murata Type 1SC | UART AT 명령 | 12mm x 12mm 초소형 모듈. 공간이 제한된 설계에 맞음. |
대역폭 예산¶
Cat-M1의 이론상 uplink 대역폭은 약 375 kbps이지만, 실측치는 약 300 kbps (~37 KB/s) 이다. XMBP 데이터를 보낼 때 이 한계를 고려해야 한다.
f32 @ 100Hz 채널 기준 계산:
Single channel data rate:
sample_size = 8 (timestamp) + 4 (f32) = 12 bytes
data_rate = 12 * 100 = 1,200 bytes/s = 1.2 KB/s
Number of channels that fit in Cat-M1 bandwidth:
usable_bandwidth = 37 KB/s (practical Cat-M1 uplink)
http_overhead_per_batch ~= 300 bytes (headers + TLS)
effective_bandwidth ~= 35 KB/s (after overhead)
max_channels = 35 / 1.2 ≈ 29 channels
With 4 channels:
data_rate = 4 * 1.2 = 4.8 KB/s
utilization = 4.8 / 37 = 13% of Cat-M1 bandwidth
--> Plenty of headroom
Cat-M1 배치 크기 권장사항:
| 파라미터 | 권장값 | 근거 |
|---|---|---|
| samples/batch | 250 | 100Hz 기준 2.5초 치 데이터. HTTP 오버헤드 대비 효율적. |
| batch interval | 2.5초 | 250 samples / 100 Hz |
| batch size (4ch f32) | ~12 KB | 14 + 4(5 + 25012) = 12,034 bytes |
| transfer time | ~325 ms | 12 KB / 37 KB/s |
| duty cycle | ~13% | 325 ms / 2500 ms |
배터리 수명을 늘리려면 배치 인터벌을 5~10초로 늘리고 samples/batch를 그에 맞게 조정한다. 샘플링 레이트 자체를 낮추는 것도 좋는다.
AT 명령 예제 (Quectel BG770A)¶
아래는 BG770A 모뎀에서 AT 명령으로 Xylolabs API와 통신하는 시퀀스이다. 실제 펌웨어에서는 각 명령 사이에 응답을 파싱하고 에러를 처리해야 한다.
1. 네트워크 등록 및 접속¶
# Check SIM and registration status
AT+CPIN? # Expect: +CPIN: READY
AT+CEREG? # Expect: +CEREG: 0,1 (registered) or 0,5 (roaming)
# Configure Cat-M1 mode (disable NB-IoT)
AT+QCFG="nwscanseq",02 # Scan LTE-M first
AT+QCFG="iotopmode",0 # LTE-M only
# Set APN for your carrier
AT+CGDCONT=1,"IP","your.carrier.apn"
# Activate PDP context
AT+QIACT=1 # Activate context 1
AT+QIACT? # Verify: shows IP address
2. XMBP 데이터 업로드 (HTTP POST)¶
# Configure HTTP for XMBP binary data upload
AT+QHTTPCFG="contextid",1
AT+QHTTPCFG="requestheader",1 # We will send custom headers
# Set the URL for the data endpoint
AT+QHTTPURL=89,10
> https://api.xylolabs.com/api/v1/ingest/sessions/dd0e8400-.../data
# Expect: OK
# Send XMBP binary batch (12034 bytes in this example)
# First, upload the request with headers
AT+QHTTPPOST=12334,30,30
> POST /api/v1/ingest/sessions/dd0e8400-.../data HTTP/1.1\r\n
> Host: api.xylolabs.com\r\n
> X-Api-Key: xk_a1b2c3d4e5f6...\r\n
> Content-Type: application/octet-stream\r\n
> Content-Length: 12034\r\n
> \r\n
> <12034 bytes of raw XMBP binary data>
# Expect: +QHTTPPOST: 0,200
# (0 = no error, 200 = HTTP 200 OK)
# Read response body
AT+QHTTPREAD=10
# Expect: {"accepted_samples":1000,"missed_batches":0}
3. 오디오 파일 업로드 (chunked)¶
# For large audio files, use chunked transfer
# First upload the file to modem's UFS storage
AT+QFUPL="RAM:audio.wav",48000,30 # Upload to modem RAM (48KB file)
# Then HTTP POST with multipart
AT+QHTTPCFG="requestheader",1
AT+QHTTPURL=52,10
> https://api.xylolabs.com/api/v1/uploads
AT+QHTTPPOSTFILE="RAM:audio.wav",30
# Note: for multipart, firmware must construct the boundary and headers
# See Quectel HTTP AT Commands manual for multipart upload details
4. PSM(Power Save Mode) 설정¶
배터리 구동 디바이스에서 Cat-M1 모뎀의 PSM을 활용하면 대기 전류를 수 uA 수준으로 줄일 수 있다.
# Enable PSM with custom timing
# T3412 (periodic TAU timer): "00100001" = 10 hours
# T3324 (active timer): "00000101" = 10 seconds
AT+CPSMS=1,,,"00100001","00000101"
# Workflow per wake cycle:
# 1. MCU wakes up, reads sensors, builds XMBP batch
# 2. AT+CFUN=1 (wake modem from PSM)
# 3. Wait for +CEREG: 1 (re-registration)
# 4. POST XMBP batch
# 5. Modem enters PSM after T3324 (10s) idle
# 6. MCU enters deep sleep until next cycle
# Check PSM status
AT+QPSMCFG?
# Expect: shows current PSM timers and mode
PSM 타이밍 참고: T3324(active timer)는 마지막 데이터 전송 후 모뎀이 깨어 있는 시간이다. XMBP POST + 응답 수신에 2~3초면 충분하므로 T3324을 10초로 설정하면 여유가 있다. T3412(TAU timer)는 배치 전송 주기보다 길게 설정하되, 너무 길면 네트워크 재등록이 오래 걸릴 수 있으니 6~24시간 사이가 적당한다.
예제: 현장 배포 워크플로¶
Cat-M1 디바이스의 현장 배포 과정은 21. 워크플로 예제의 Cat-M1 디바이스 현장 배포 워크플로에서 bash 스크립트로 다루고 있다. 관리자 쪽 프로비저닝(시설, 키 생성)부터 디바이스 쪽 동작(세션 생성, XMBP 전송, 세션 종료, PSM 진입), 다시 관리자 쪽 데이터 확인까지 전체 흐름이 담겨 있다.
요약하면 다음과 같는다.
- 관리자: 시설 생성, API 키 발급, 디바이스 사전 등록
- 디바이스 펌웨어: 부팅 -> AT 명령으로 네트워크 접속 -> 세션 생성 -> XMBP 배치 전송 (2.5초 간격) -> 세션 종료 -> PSM 진입
- 관리자: 대시보드에서 수집 데이터 확인, CSV 내보내기로 분석