1. 이건 무엇인가 — 역할

AEB의 MQTT 브리지는 범용·수신 전용(SUBSCRIBER-only) 브리지입니다. 외부 MQTT 브로커에 mTLS로 접속해 토픽을 구독하고, 받은 메시지를 같은 LAN의 SmartThings Hub Edge 드라이버로 HTTP POST 포워딩합니다. 특정 벤더(LG·AWS 등)를 전혀 알지 못하는 범용 파이프입니다 — 인증서·토픽·clientId만 주면 됩니다.

하는 일

  • 드라이버가 준 인증서(mTLS)로 외부 브로커에 접속·구독.
  • 수신한 메시지를 가공 없이 그대로 Hub Edge 드라이버로 HTTP 포워딩.
  • 연결·재시도·버퍼링 같은 장기 연결 살림을 대신 떠맡음 (Edge 드라이버는 이걸 못 함).

하지 않는 일

  • publish 안 함 — 단방향 수신 전용. (메시지를 브로커로 보내지 않음.)
  • 페이로드를 파싱·변형하지 않음 — 원본 문자열 그대로 전달(바이너리는 base64).
  • 인증서 서명·발급 안 함 — CSR만 만들고 서명은 외부 CA(예: ThinQ) 몫.
왜 필요한가? SmartThings Edge 드라이버는 허브 안 Lua 샌드박스에서 돌아 외부 인터넷으로의 장기 MQTT 연결을 유지할 수 없습니다. 같은 LAN의 AEB(안드로이드 기기)가 그 연결을 대신 들고 있다가, 메시지가 오면 LAN HTTP로 허브에 넣어 줍니다.

1. What this is — the role

AEB's MQTT bridge is a general-purpose, receive-only (SUBSCRIBER) bridge. It connects to an external MQTT broker over mTLS, subscribes to topics, and HTTP-POST forwards received messages to a SmartThings Hub Edge driver on the same LAN. It is a generic pipe that knows nothing about any vendor (LG, AWS, …) — you just hand it a certificate, topics, and a clientId.

What it does

  • Connects & subscribes to an external broker using the driver-supplied certificate (mTLS).
  • HTTP-forwards each received message, untouched, to the Hub Edge driver.
  • Owns the long-lived-connection chores — connect, retry, buffering — that an Edge driver cannot.

What it does not do

  • Never publishes — one-way, receive-only. (It does not send messages to the broker.)
  • Does not parse or transform payloads — forwards the raw string (base64 if binary).
  • Does not sign or issue certificates — it only produces a CSR; an external CA (e.g. ThinQ) signs it.
Why is it needed? A SmartThings Edge driver runs in a Lua sandbox inside the hub and cannot hold a long-lived MQTT connection to the internet. AEB (an Android device) on the same LAN holds that connection instead and, when a message arrives, pushes it to the hub over LAN HTTP.

2. 아키텍처 / 데이터 흐름

메시지는 한 방향으로만 흐릅니다: 브로커 → AEB → 허브 드라이버.

2. Architecture / data flow

Messages flow one way only: broker → AEB → hub driver.

① MQTT Broker — AWS IoT · LG ThinQ · 일반 MQTTS (mTLS, :8883)
│ subscribe + receive (mTLS) ▼
② AEB (Android) — 구독 유지, 메시지 큐잉(링버퍼 200), at-least-once 포워딩
│ HTTP POST /aeb/ingest ▼
③ SmartThings Hub Edge Driver — 메시지 수신 → seq 중복제거 → capability emit
① MQTT Broker — AWS IoT · LG ThinQ · generic MQTTS (mTLS, :8883)
│ subscribe + receive (mTLS) ▼
② AEB (Android) — keeps the subscription, queues messages (ring buffer 200), at-least-once forwarding
│ HTTP POST /aeb/ingest ▼
③ SmartThings Hub Edge Driver — receive message → seq de-dup → capability emit

제어 평면(세션 생성·connect·forward 등록)도 같은 방향입니다: 드라이버가 LAN HTTP로 AEB의 /mqtt/* API를 호출합니다. 데이터 평면(메시지)만 AEB→허브로 흐릅니다.

The control plane (create session, connect, register forward) runs the same way: the driver calls AEB's /mqtt/* API over LAN HTTP. Only the data plane (messages) flows AEB→hub.

3. REST API 레퍼런스

AEB는 LAN에 로컬 HTTP API를 노출합니다(기본 포트 8088). 모든 요청에 헤더 X-AEB-Api-Version: 1이 필요합니다. 아래는 Interface-Spec v0.3 §4 와 일치합니다.

3. REST API reference

AEB exposes a local HTTP API on the LAN (default port 8088). Every request requires the header X-AEB-Api-Version: 1. The below matches Interface-Spec v0.3 §4.

Method 경로Path 역할Role
POST/mqtt/sessions 세션 생성 + RSA2048 키쌍 + PKCS#10 CSR(PEM) 발급 Create session + RSA2048 keypair + PKCS#10 CSR (PEM)
POST/mqtt/sessions/{id}/connect 브로커 mTLS 접속 + 토픽 구독 mTLS connect to broker + subscribe to topics
PUT/mqtt/sessions/{id}/forward 포워드 타깃 등록(멱등) → http://<hub>:port/path Register forward target (idempotent) → http://<hub>:port/path
GET/mqtt/sessions/{id}/status 상태 + 진단 조회 Read state + diagnostics
DELETE/mqtt/sessions/{id} 세션 종료 + 키·인증서 삭제 Terminate session + erase keys/certs
GET/mqtt/sessions/{id}/messages?since= (선택) 캐치업 폴링 — 버퍼된 메시지 페이지 조회 (optional) Catch-up polling — page through buffered messages

POST /mqtt/sessions

요청 본문은 선택. 응답에서 받은 csrPem을 외부 CA로 서명해 connectcertPem으로 씁니다. 개인키는 AEB 안에만 남고 절대 응답에 포함되지 않습니다.

Request body optional. Sign the returned csrPem with an external CA and use it as certPem in connect. The private key stays inside AEB and is never returned.

// request (optional)
{ "keyType": "RSA2048", "subjectCN": "AWS IoT Certificate" }

// 201 response
{ "sessionId": "sess_ab12cd34ef56", "csrPem": "-----BEGIN CERTIFICATE REQUEST-----\n...", "state": "CREATED" }

POST /mqtt/sessions/{id}/connect

qos는 0 또는 1만 허용(2 미지원). topics는 최소 1개, 와일드카드 +·# 가능. caPem 생략 시 시스템 신뢰 저장소 사용. clientId 생략 시 aeb-<sessionId> 사용 — AWS IoT/ThinQ는 정책에 맞는 clientId가 필요합니다.

qos is 0 or 1 only (2 unsupported). topics needs ≥1 entry; wildcards +/# allowed. If caPem is omitted the system trust store is used. If clientId is omitted, aeb-<sessionId> is used — AWS IoT/ThinQ require a policy-matching clientId.

// request
{
  "certPem":     "-----BEGIN CERTIFICATE-----\n...",
  "caPem":       "-----BEGIN CERTIFICATE-----\n...",   // optional
  "endpoint":    "xxxx-ats.iot.ap-northeast-2.amazonaws.com",
  "port":        8883,
  "topics":      ["device/+/state", "device/status"],
  "qos":         1,
  "keepAliveSec": 60,
  "clientId":    "my-device-id"                         // optional
}

// 200 response
{ "sessionId": "sess_ab12cd34ef56", "state": "CONNECTING", "subscribedTopics": ["device/+/state","device/status"] }

PUT /mqtt/sessions/{id}/forward

멱등. hubAddress를 생략하면 AEB가 요청 소스 IP(=허브)를 사용합니다. path 기본값 /aeb/ingest.

Idempotent. If hubAddress is omitted, AEB uses the request source IP (= the hub). path defaults to /aeb/ingest.

// request
{ "hubPort": 12345, "path": "/aeb/ingest", "hubAddress": null }

// 200 response
{ "sessionId": "sess_ab12cd34ef56", "forwardTarget": "http://192.168.0.20:12345/aeb/ingest" }

GET /mqtt/sessions/{id}/status

{
  "sessionId":         "sess_ab12cd34ef56",
  "state":             "CONNECTED",
  "subscribedTopics":  ["device/+/state", "device/status"],
  "forwardTarget":     "http://192.168.0.20:12345/aeb/ingest",
  "pendingForwardCount": 0,
  "lastConnectedTs":   1717689874000,
  "lastForwardOkTs":   1717689900000,
  "lastError":         null,
  "effectiveClientId": "my-device-id",   // actual clientId of the CONNECT (aeb-<id> if fallback)
  "liveClientIdConnections": 1,           // concurrent connections using the same clientId (1=normal, ≥2=duplicate)
  "forwardFailureCount": 3,               // consecutive forward-delivery failures since last success (0 = omitted)
  "lastForwardErrorTs": 1717690000000,    // last delivery failure time (absent if none)
  "forwardTargetHealthy": false           // false = deliveries are failing now, re-register /forward (true = omitted)
}
공통 에러 형식
Common error shape
{ "error": { "code": "SESSION_NOT_FOUND", "message": "..." } }

4. 세션 상태머신

세션은 다음 상태를 거칩니다. statusstate 필드로 노출됩니다.

4. Session state machine

A session moves through these states, exposed as the state field of status.

CREATED → CONNECTING → CONNECTED → DISCONNECTED | ERROR

5. 메시지 포워드 스펙

CONNECTED 상태에서 받은 각 MQTT 메시지는 아래 JSON으로 포워드 타깃에 POST됩니다(Interface-Spec v0.3 §5).

5. Message forward spec

While CONNECTED, each received MQTT message is POSTed to the forward target as the JSON below (Interface-Spec v0.3 §5).

// POST <forwardTarget>  body:
{
  "sessionId":       "sess_ab12cd34ef56",
  "seq":             1,
  "topic":           "device/abc/state",
  "payload":         "{\"power\":\"on\"}",
  "payloadEncoding": "utf8",     // "utf8" | "base64"
  "ts":              1717689900000
}

6. 보안 / mTLS

  • 개인키 비노출 — RSA2048 키쌍은 세션 생성 시 기기 안에서 생성·암호화 저장(Android EncryptedSharedPreferences). 어떤 API 응답에도 들어가지 않음. 드라이버에는 CSR만 발급.
  • CA 핀닝caPem을 주면 그 CA만 신뢰. 생략하면 시스템 신뢰 저장소 사용.
  • SNI — AEB는 브로커 호스트명(endpoint)을 TLS SNI로 전송합니다. AWS IoT 등 일부 브로커는 SNI를 요구합니다.
  • clientId 정책 — AWS IoT/ThinQ는 IoT 정책에 묶인 특정 clientId를 요구합니다. connect 시 정책에 맞는 clientId를 넣어 주세요.

6. Security / mTLS

  • Private key never leaves — the RSA2048 keypair is generated and stored encrypted on-device (Android EncryptedSharedPreferences) at session creation. It is in no API response; the driver only ever gets a CSR.
  • CA pinning — provide caPem to trust only that CA; omit it to use the system trust store.
  • SNI — AEB sends the broker hostname (endpoint) as the TLS SNI. Some brokers (e.g. AWS IoT) require it.
  • clientId policy — AWS IoT/ThinQ require a specific clientId bound to the IoT policy. Send a policy-matching clientId on connect.
스펙 한 파일로 받기 — 드라이버를 붙일 때 코드 옆에 두고 볼 수 있는 단일 파일: mqtt-bridge-spec-v0.3.md (Interface-Spec v0.3)
Get the spec as one file — a single file to keep next to your code while wiring up a driver: mqtt-bridge-spec-v0.3.md (Interface-Spec v0.3)