제로웍스 연동 API 문서

제로웍스 로봇이 비스캣 관제와 주고받는 채널·토픽·페이로드·인증 규약입니다. 동작 흐름은 개발 시나리오를 함께 참조하세요.

본 문서의 JSON 본문은 예시 값이며, 값 자체에 의미를 두지 않습니다.

2026-09-26 개정 요약

2026-09-03 개정 요약

1. 공통 규약

requestId — 텔레메트리는 생략 가능

상태(state)처럼 자발·고빈도로 발행되는 텔레메트리는 최신값만 의미가 있어 requestId를 생략할 수 있고, 관제는 occurredAt 비교로 더 최신일 때만 반영(last-write-wins)합니다. 반면 행위를 트리거하는 이벤트·명령(입찰 bid, 알람, PIN, 박스 닫힘 등)은 at-least-once 재전송에 대비해 requestId(또는 동등 식별자)를 유지해 멱등 처리합니다.

status 어휘 — 전 계층 standby · doing · done 2026-09-03 개정

상태값은 어느 계층에서든 같은 낱말을 씁니다. 계층은 무엇의 상태인가만 다르고 어휘는 공유합니다.

계층위치standbydoingdone
로봇state.status일감 없음 · 대기일감 수행 중수행 완료(다음 일감 전)
goalgoals[].status부여됐으나 아직 이동 전해당 지점으로 이동 중도착·해당 지점 처리 완료
missiongoals[].mission[].status아직 시작 전수행 중수행 완료

취소는 세 계층 모두 canceled이며 사유(reason·reasonMsg)를 함께 싣습니다(로봇 발행 항목 §6).

폐지된 표기대체
ready (goal·mission)standby
going (goal)doing
arrived (goal)done — goal 에 별도 종료값을 두지 않습니다
paused (state)사용하지 않습니다 — 일시정지는 알람(§4.2)으로 표현

관제 수신부는 문자열을 그대로 보관하므로 미확정 값이 와도 파싱은 깨지지 않지만, 의미 해석(도착 통지·완료 판정)은 위 세 값에만 걸려 있습니다. 실측상 unused·빈 문자열이 state.status로 관측된 적이 있어(2026-08 아카이브) 위 세 값으로 정리 부탁드립니다.

2. 인증 (REST)

등록·인증은 REST로 진행합니다. 키쌍은 로봇이 직접 생성하며, 개인키는 디바이스 밖으로 내보내지 않습니다(TPM·보안 칩 권장).

키 발급 절차

  1. 키쌍 생성 — 로봇이 공개키/개인키 쌍을 디바이스 내부에서 생성.
  2. 공개키 제출 — 사전 등록된 robotCode(예: R-001)와 함께 로봇의 공개키를 POST /v1/robot/key로 제출. 관제가 robotCode 로 로봇을 조회해 내부 식별자 robotId(UUID)를 회신하며, 로봇은 이후 인증에 robotId 를 사용한다.
  3. 비밀키 수령 — 서버가 HMAC 비밀키를 로봇 공개키로 암호화해 응답 → 로봇이 개인키로 복호화해 보관.
  4. 인증서 발급 — POST /v1/robot/cert로 X.509 인증서 번들(다운로드 URL 목록) 수령 → 설치 후 MQTT 연결.
  5. 요청 서명 — 이후 모든 REST 인증 요청에 HMAC-SHA256 서명 헤더 부착.

암호화·서명 규격 비스캣 제안 — 협의 확정 대상

로봇(Ubuntu·C++/OpenSSL)과 관제가 상호운용하도록 아래 규격을 제안합니다. 모든 문자열은 UTF-8, base64는 표준(RFC 4648, 개행 없음)입니다.

키쌍RSA-2048. 공개키(publicKey)는 DER SubjectPublicKeyInfo(SPKI)를 base64 인코딩(PEM 헤더·개행 없음). OpenSSL i2d_PUBKEY → base64.
비밀키 암호화
(secretKeyEncrypted)
RSA-OAEP — 해시 SHA-256 · MGF1 SHA-256 · label 없음. 서버가 로봇 공개키로 암호화한 base64 를 로봇이 개인키로 복호화.
OpenSSL: RSA_PKCS1_OAEP_PADDING + set_rsa_oaep_md(sha256) + set_rsa_mgf1_md(sha256).
HMAC 비밀키복호화 결과 문자열을 그대로(ASCII 바이트) HMAC 키로 사용 — base64 디코드 금지.
서명HMAC-SHA256, 출력은 소문자 hex. X-Robot-Signature = HMAC-SHA256(secret, 정규화문자열).
정규화 문자열
(서명 대상)
{METHOD}\n{path}\n{X-Robot-Ts}\n{sha256hex(body)} — METHOD 대문자, 구분자 \n(0x0A), sha256hex(body) = 요청 본문 바이트의 SHA-256 소문자 hex.
인증 헤더X-Robot-ID(robotId UUID) · X-Robot-Ts(ISO-8601 +09:00) · X-Robot-Signature(위 HMAC hex)
타임스탬프ISO-8601 KST(예 2026-07-08T14:30:00+09:00). 서버 시각과 ±60초 이내(NTP 동기 필요) — 초과 시 ROBOT_TIMESTAMP_OUT_OF_WINDOW.

위 값은 비스캣 제안 규격이며 제로웍스 확인 후 확정합니다. 인증서 발급은 현재 서버 생성(원클릭) + 다운로드 URL 번들 방식이며, 필요 시 CSR 서명 방식(로봇이 mTLS 키 생성·CSR 제출, 개인키 미전송)으로 협의 가능합니다. 협의 확정 대상

POST /v1/robot/key — 비밀키 발급

인증없음 (사전 등록된 robotCode + 공개키)
요청
{ "robotCode": "R-001", "publicKey": "base64(SPKI DER)" }   // robotCode = 관리자 등록 식별자, publicKey = RSA-2048 공개키(DER SubjectPublicKeyInfo → base64)
200 응답
{
  "result": "SUCCESS",
  "data": {
    "robotId": "uuid",                // 관제가 robotCode 로 조회해 반환하는 내부 식별자 — 이후 인증·MQTT 에 사용
    "secretKeyEncrypted": "base64",   // 로봇 공개키로 암호화된 HMAC 비밀키
    "issuedAt": "2026-05-21T10:00:00+09:00"
  }
}
재발급이미 발급(활성화)된 로봇이 다시 호출하면 409 ROBOT_ALREADY_ACTIVATED로 거절됩니다. 재발급이 필요하면 관제 관리자 초기화 후 다시 발급받습니다(이전 Thing·인증서는 폐기).
에러404 ROBOT_NOT_REGISTERED(robotCode 미등록), 422 ROBOT_PUBLIC_KEY_INVALID(공개키 형식 오류)

POST /v1/robot/cert — X.509 인증서 발급 (다운로드 URL 목록)

관제가 AWS IoT Core에 로봇 Thing(thingName = robotId)과 X.509 인증서를 발급하고, 인증서 번들을 저장한 뒤 파일별 다운로드 URL 목록을 회신합니다. 로봇은 각 URL로 파일을 내려받아 설치하고 mTLS로 MQTT에 연결합니다.

인증HMAC (로봇)
번들 파일certificate.pem.crt(디바이스 인증서) · private.pem.key(개인키) · public.pem.key(공개키) · AmazonRootCA1.pem(서버 검증 CA)
200 응답
{
  "result": "SUCCESS",
  "data": {
    "files": [                          // 번들 파일별 다운로드 URL
      { "name": "certificate.pem.crt", "url": "https://.../certificate.pem.crt?..." },
      { "name": "private.pem.key",     "url": "https://..." },
      { "name": "public.pem.key",      "url": "https://..." },
      { "name": "AmazonRootCA1.pem",   "url": "https://..." }
    ],
    "expiresIn": 600                    // 다운로드 URL 유효기간(초)
  }
}

발급 인증서에는 로봇 정책(zws-iot-policy-fms, Thing 변수 기반)이 부착되어 로봇은 자기 토픽만 pub/sub 합니다. 번들은 암호화 저장되며, 다운로드 URL은 expiresIn 경과 시 만료됩니다.

재발급 정책 — 로봇은 등록·인증으로 비밀키·인증서를 발급받으며, 이미 발급(활성화)된 로봇이 /key·/cert를 다시 호출하면 409 ROBOT_ALREADY_ACTIVATED로 거절됩니다. 재발급(인증서 회전)이 필요하면 관제 관리자 초기화 후 다시 발급받으며, 이때 이전 Thing·인증서·번들은 폐기됩니다.

HMAC 서명 방식

발급받은 비밀키로 요청을 서명합니다. X-Robot-Signature = HMAC-SHA256(secret, 정규화문자열)(소문자 hex), 정규화 문자열은 {METHOD}\n{path}\n{X-Robot-Ts}\n{sha256hex(body)}입니다. X-Robot-Ts는 ±60초 윈도우를 벗어나면 ROBOT_TIMESTAMP_OUT_OF_WINDOW로 거절(replay 방지)됩니다.

예) POST /v1/robot/cert · body {} → 정규화 문자열:

POST
/v1/robot/cert
2026-07-08T14:30:00+09:00
44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a

마지막 줄은 sha256hex("{}"). 정규화 문자열의 줄 구분자는 \n(LF)입니다.

C++/OpenSSL 레퍼런스 구현 비스캣 제안

로봇(Ubuntu · C++)에서 위 규격을 구현하는 최소 예제입니다. OpenSSL 3.x 기준(빌드: g++ auth.cpp -lssl -lcrypto)이며, 지면상 에러 처리는 생략했습니다. HTTP 호출·JSON 파싱은 libcurl·nlohmann/json 등 임의 라이브러리를 사용합니다.

① 공통 헬퍼 (base64 · hex · SHA-256 · HMAC)

#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <string>
#include <vector>
#include <stdexcept>

static std::string toHex(const unsigned char* d, size_t n) {
    static const char* h = "0123456789abcdef";
    std::string s; s.reserve(n * 2);
    for (size_t i = 0; i < n; ++i) { s += h[d[i] >> 4]; s += h[d[i] & 0xf]; }
    return s;
}

// 표준 base64(RFC 4648, 개행 없음)
static std::string b64encode(const unsigned char* d, int n) {
    std::string out(4 * ((n + 2) / 3), '\0');
    int len = EVP_EncodeBlock((unsigned char*)out.data(), d, n);
    out.resize(len);
    return out;
}
static std::vector<unsigned char> b64decode(const std::string& in) {
    std::vector<unsigned char> out(3 * (in.size() / 4) + 3);
    int len = EVP_DecodeBlock(out.data(), (const unsigned char*)in.data(), (int)in.size());
    if (len < 0) throw std::runtime_error("base64 decode 실패");
    if (!in.empty() && in.back() == '=') len--;            // 패딩 보정
    if (in.size() >= 2 && in[in.size() - 2] == '=') len--;
    out.resize(len);
    return out;
}

static std::string sha256Hex(const std::string& data) {
    unsigned char md[EVP_MAX_MD_SIZE]; unsigned int n = 0;
    EVP_Digest(data.data(), data.size(), md, &n, EVP_sha256(), nullptr);
    return toHex(md, n);
}
static std::string hmacSha256Hex(const std::string& key, const std::string& msg) {
    unsigned char mac[EVP_MAX_MD_SIZE]; unsigned int n = 0;
    HMAC(EVP_sha256(), key.data(), (int)key.size(),
         (const unsigned char*)msg.data(), msg.size(), mac, &n);
    return toHex(mac, n);
}

② 키쌍 생성 · 공개키 인코딩

#include <openssl/rsa.h>
#include <openssl/x509.h>   // i2d_PUBKEY

// RSA-2048 키쌍 생성. 개인키는 디바이스에 안전 저장(예: PEM_write_PrivateKey_ex), 외부 반출 금지.
EVP_PKEY* generateRobotKeyPair() { return EVP_RSA_gen(2048); }   // OpenSSL 3.0+

// 공개키 → base64(SPKI DER). POST /v1/robot/key 의 publicKey 필드에 그대로 사용.
std::string publicKeyB64(EVP_PKEY* pkey) {
    unsigned char* der = nullptr;
    int len = i2d_PUBKEY(pkey, &der);        // SubjectPublicKeyInfo(DER)
    if (len <= 0) throw std::runtime_error("i2d_PUBKEY 실패");
    std::string b64 = b64encode(der, len);
    OPENSSL_free(der);
    return b64;
}

③ 비밀키 복호화 (RSA-OAEP · SHA-256/MGF1 SHA-256)

// secretKeyEncrypted(base64) → HMAC secret 문자열. OAEP 해시 SHA-256 / MGF1 SHA-256.
std::string decryptSecret(EVP_PKEY* priv, const std::string& secretKeyEncryptedB64) {
    std::vector<unsigned char> ct = b64decode(secretKeyEncryptedB64);
    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(priv, nullptr);
    EVP_PKEY_decrypt_init(ctx);
    EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
    EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256());
    EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256());   // ← SHA-256 로 명시(서버와 일치)
    size_t outLen = 0;
    EVP_PKEY_decrypt(ctx, nullptr, &outLen, ct.data(), ct.size());
    std::string secret(outLen, '\0');
    EVP_PKEY_decrypt(ctx, (unsigned char*)secret.data(), &outLen, ct.data(), ct.size());
    secret.resize(outLen);
    EVP_PKEY_CTX_free(ctx);
    return secret;   // 그대로 HMAC 키로 사용 — base64 디코드 금지
}

④ 타임스탬프 · HMAC 서명 헤더

#include <ctime>

// ISO-8601 KST (예: 2026-07-08T14:30:00+09:00). NTP 로 시각 동기 필요(±60초).
std::string nowKstIso8601() {
    std::time_t t = std::time(nullptr) + 9 * 3600;   // UTC+9
    std::tm tm{}; gmtime_r(&t, &tm);
    char buf[40];
    std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S+09:00", &tm);
    return buf;
}

// POST /v1/robot/cert 서명 헤더 구성.
void buildCertHeaders(const std::string& robotId, const std::string& secret,
                      std::string& xId, std::string& xTs, std::string& xSig, std::string& body) {
    body = "{}";
    xId  = robotId;
    xTs  = nowKstIso8601();
    // 정규화: {METHOD}\n{path}\n{X-Robot-Ts}\n{sha256hex(body)}
    std::string canonical = "POST\n/v1/robot/cert\n" + xTs + "\n" + sha256Hex(body);
    xSig = hmacSha256Hex(secret, canonical);
}

⑤ 호출 순서

int main() {
    EVP_PKEY* priv = generateRobotKeyPair();
    std::string pub = publicKeyB64(priv);

    // 1) POST /v1/robot/key   body: {"robotCode":"R-001","publicKey":pub}
    //    응답(data): robotId, secretKeyEncrypted        (HTTP·JSON 은 libcurl·nlohmann/json 등)
    std::string robotId   = /* 응답 파싱 */ "";
    std::string secretEnc = /* 응답 파싱 */ "";

    std::string secret = decryptSecret(priv, secretEnc);   // HMAC 키 확보

    // 2) POST /v1/robot/cert  (서명 헤더 + body {})
    std::string xId, xTs, xSig, body;
    buildCertHeaders(robotId, secret, xId, xTs, xSig, body);
    //   헤더:  X-Robot-ID: xId  ·  X-Robot-Ts: xTs  ·  X-Robot-Signature: xSig
    //   응답(data.files[]) 의 다운로드 URL 을 일반 HTTPS GET 으로 각각 다운로드 →
    //   certificate.pem.crt · private.pem.key · AmazonRootCA1.pem 으로 mTLS MQTT 접속(clientId=robotId).
    return 0;
}

3. MQTT 채널·토픽 네임스페이스 협의 대상

인증서 설치 후 로봇은 MQTT 브로커에 연결합니다. 토픽은 방향 프리픽스를 앞에 둔 {방향}/{robot_id}/{항목}/{명령} 형태입니다 — s2r(server→robot, 로봇 subscribe), r2s(robot→server, 로봇 publish). 브로커 정책으로 로봇은 r2s/{자기id}/#만 publish, s2r/{자기id}/#만 subscribe할 수 있어 디바이스 토픽이 격리됩니다.

토픽방향용도QoS
s2r/{robot_id}/state/get관제 → 로봇상태 질의 (조회 필드 지정) 예약 — 관제 미발행1
s2r/{robot_id}/state/set관제 → 로봇상태 설정 (변동 필드만). 배차 주행 개시에는 쓰지 않습니다 — §5.3 ③ 참조1
r2s/{robot_id}/state로봇 → 관제상태 발행 (변동 필드 이벤트, retain)0
r2s/{robot_id}/alarm로봇 → 관제알람 발행1
s2r/{robot_id}/alarm관제 → 로봇알람 리셋 예약 — 관제 미발행1
s2r/{robot_id}/goals/get관제 → 로봇goal 질의 예약 — 관제 미발행1
s2r/{robot_id}/goals/set관제 → 로봇부여된 goal·mission 변경 지시(부분 갱신). goal 부여는 offer→result 경로가 담당 — §5.11
r2s/{robot_id}/goals로봇 → 관제goal 수행 상태 발행 (진행 중 미션 전체 스냅샷)1
s2r/{robot_id}/goals/offer관제 → 로봇입찰 요청 — goal 전체 정의를 실어 보냄(§5.1 본문)1
r2s/{robot_id}/goals/bid로봇 → 관제입찰 응답 — quote/bid와 동일 스키마(§5.3 ②)1
s2r/{robot_id}/goals/result관제 → 로봇입찰 결과 (낙찰 통보 = 주행 개시 신호)1
s2r/{robot_id}/quote/offer관제 → 로봇배송 가능여부 견적 질의 (비구속). 본문은 goals/offer와 다른 축약형 — §5.41
r2s/{robot_id}/quote/bid로봇 → 관제견적 응답 — goals/bid와 동일 스키마(§5.4 ②)1
r2s/{robot_id}/resource로봇 → 관제인프라(엘베/로비폰) 요청 — 로비폰은 orderId+lobbyId, 엘베는 orderId+elevatorCode+탑승층·목적층. 탑승·하차 보고와 엘베 콜 취소(status: "cancelByRobot")도 같은 토픽1
s2r/{robot_id}/resource/ready관제 → 로봇요청 처리 완료 → 진행 신호. 단 취소 회신(result: "canceled")은 진행이 아니라 종결 신호1
s2r/{robot_id}/resource/status관제 → 로봇엘베 중간 상태(serviceStatus) + 배정 호기(elId) 중계1
s2r/{robot_id}/box/open관제 → 로봇박스 개방 지시 (오토메타 웹 개방·상차/하차) §6.11
r2s/{robot_id}/safety로봇 → 관제보행자 안전 이벤트 (인지·회피·정지·재가동) — 로봇 발행 항목 §41
r2s/{robot_id}/software로봇 → 관제소프트웨어 인벤토리 (컴포넌트별 버전, retain) — 로봇 발행 항목 §81
s2r/{robot_id}/geofence/version관제 → 로봇단지 지오펜스 버전 알림 (retain). 본문은 REST로 조회 — §81
r2s/{robot_id}/geofence로봇 → 관제영역에 의한 우회·차단 이벤트 — 로봇 발행 항목 §91
s2r/{robot_id}/action/set관제 → 로봇운영자 개입 명령 (정지·재가동·속도·이동·회피) — §3.11
r2s/{robot_id}/ack로봇 → 관제s2r 지시 수신·판정 회신 — §3.21
s2r/{robot_id}/software/deploy관제 → 로봇소프트웨어 배포 지시 — §3.31
r2s/{robot_id}/software/progress로봇 → 관제배포 진행률·결과 — §3.31
폐지 — r2s/{robot_id}/box/loaded · r2s/{robot_id}/box/unloaded: 상차·하차 완료는 r2s/{robot_id}/goals의 pickup:done·dropoff:done으로 일원화했습니다(§6.1). 관제는 두 토픽을 구독하지 않습니다.

네임스페이스 확정 항목

3.1 운영자 개입 명령 — s2r/{robot_id}/action/set 비스캣 제안

운영자가 관제 화면에서 내리는 개입 지시를 로봇에 전달합니다. 미션 경로(§5)와 분리된 채널입니다 — 미션 원장의 상태를 바꾸는 것이 아니라 로봇의 현재 거동에 직접 개입하는 명령이기 때문입니다. 미션 자체를 끊는 것은 이 토픽이 아니라 goals/set이 담당합니다(아래 callout).

{
  "requestId": "uuid-v7",
  "occurredAt": "2026-09-26T14:00:00+09:00",
  "payload": {
    "action": "speed",                   // 개입 종류
    "params": { "limitKmh": 1.5 }        // action 별 인자. estop·resume 은 생략
  }
}
actionparams로봇 동작
estop—즉시 정지. 정지 후 state.ems에 remote를 실어 보고합니다(§4 어휘 확장). 미션은 유지되며 goal.status는 바뀌지 않습니다
resume—estop 해제. 직전 수행 중이던 goal 을 이어서 수행합니다. state.ems에서 remote를 제거해 보고합니다
speed{ "limitKmh": number }주행 속도 상한을 지정합니다. 상한 이하 구간의 로봇 자체 감속은 그대로 동작합니다. limitKmh 생략 시 상한 해제
goto{ "location": ZwsLocation }지정 좌표·노드로 이동합니다. 미션 수행 중에는 거절합니다(reason: "mission-in-progress") — 미션 목적지 변경은 goals/set이 담당합니다
reroute{ "avoidRadiusM": number }현재 위치 전방의 지정 반경을 회피해 경로를 재계획합니다. 목적지는 바뀌지 않습니다

location 본문 규격은 §5.1 goals[].location과 같습니다(맵 로컬 좌표 x·y·th + map_id).

estop은 다른 s2r보다 먼저 처리합니다

안전 명령이므로 수신 큐에서 다른 s2r 지시를 앞질러 처리합니다. 관제 발행부터 로봇 정지까지 허용 지연은 500ms이며, 초과 시 관제는 통신 경고를 올립니다. 운용 중 조정

수신 확인은 §3.2 ack로 회신합니다 — estop은 정지 완료 시점에 result: "done"을 보냅니다. 관제 화면이 「지시를 보냈다」와 「로봇이 멈췄다」를 구분해 표시하는 근거가 이 회신입니다.

진행 중 미션 중단은 goals/set입니다 — action/set이 아닙니다

미션을 끊는 것은 로봇 거동 개입이 아니라 미션 원장의 상태 변경이므로, 상태 변경 채널인 s2r/{robot_id}/goals/set에 해당 goal·mission 의 status: "canceled"를 실어 보냅니다(§5.1 · §1 status 어휘). 전용 취소 토픽을 두지 않습니다.

로봇은 canceled를 받으면 해당 goal 의 주행을 종료하고, 적재물이 있으면 mode=navi + goal.type=unloading으로 하역장 회수 경로에 진입합니다. 수신 확인은 ack로 회신합니다.

3.2 지시 수신 확인 — r2s/{robot_id}/ack 비스캣 제안

requestId를 실어 보내는 모든 s2r 지시에 대해 로봇이 받았는지·수행했는지·거절했는지를 회신합니다. 관제는 이 회신으로 지시의 종결을 판정하며, 회신이 없으면 「전송됨」에서 더 나아가지 않습니다.

{
  "requestId": "uuid-v7",              // 대상 s2r 지시의 requestId — 변형 없이 echo
  "occurredAt": "2026-09-26T14:00:00.480+09:00",
  "payload": {
    "topic": "action/set",             // 대상 토픽 (방향 프리픽스 제외)
    "result": "accepted",              // accepted | rejected | done
    "reason": null                     // rejected 일 때 사유 코드 (§10)
  }
}
필드타입설명
requestIdstring대상 s2r 지시의 requestId를 변형 없이 되돌립니다 — 관제가 이 값으로 지시를 역인덱싱합니다
topicstring대상 토픽의 항목 부분(action/set·goals/set·box/open·resource/ready·software/deploy)
resultenumaccepted(받아 수행 시작) · rejected(수행 불가) · done(수행 완료)
reasonstring | nullrejected일 때 사유 코드. 어휘는 §10

발행 시점 — 두 단계로 나눕니다

수행 결과 자체(도착·미션 진행)는 기존 r2s/{robot_id}/goals·state로 발행합니다 — ack는 지시의 종결만 다룹니다. retain 을 쓰지 않습니다(일회성 이벤트).

3.3 소프트웨어 배포 — software/deploy · software/progress 비스캣 제안

관제가 컴포넌트별 버전을 지정해 배포를 지시하고, 로봇이 진행률을 보고합니다. 설치 결과는 기존 r2s/{robot_id}/software(인벤토리, retain)의 재발행으로 확정됩니다 — 배포 원장과 설치 현황이 두 곳으로 갈리지 않게 합니다.

① 배포 지시 — s2r/{robot_id}/software/deploy

{
  "requestId": "uuid-v7",
  "occurredAt": "2026-09-26T14:00:00+09:00",
  "payload": {
    "jobId": "ZWS-FWJ-...",              // 관제 발급 — progress 에 echo
    "components": [
      { "name": "navigation", "version": "1.4.2",
        "url": "https://.../navigation-1.4.2.tar.gz",
        "sha256": "9f2c..." }
    ],
    "scheduledAt": null                  // null = 즉시. 값이 있으면 그 시각 이후 설치
  }
}
필드타입설명
jobIdstring배포 회차 식별자 — 관제가 발급하고 progress가 그대로 되돌립니다
components[].namestringr2s/{robot_id}/software 인벤토리의 컴포넌트 이름과 같은 값
components[].versionstring설치 목표 버전
components[].urlstring패키지 내려받기 주소(만료 있는 서명 URL)
components[].sha256string무결성 검증값. 불일치 시 설치하지 않고 failed로 보고합니다
scheduledAtstring | null예약 설치 시각(ISO-8601). null이면 즉시

주행 중에는 설치하지 않습니다 — 로봇은 미션이 없고 정지 상태일 때 설치하며, 지시 수신 시점이 주행 중이면 ack로 accepted를 보낸 뒤 대기합니다.

② 진행률·결과 — r2s/{robot_id}/software/progress

{
  "requestId": "uuid-v7",
  "occurredAt": "2026-09-26T14:03:12+09:00",
  "payload": {
    "jobId": "ZWS-FWJ-...",
    "component": "navigation",
    "progress": 64,                      // 0~100
    "status": "doing",                   // standby | doing | done | failed
    "reason": null                       // failed 일 때 사유 코드 (§10)
  }
}

컴포넌트 단위로 발행합니다. status=done 뒤에는 r2s/{robot_id}/software 인벤토리를 갱신 발행해 설치 버전을 확정합니다. failed는 해당 컴포넌트의 이전 버전이 유지됨을 뜻합니다(부분 적용 없음).

4. robot info — 상태·알람

4.1 상태 질의 — s2r/{robot_id}/state/get 예약 — 관제 미발행

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": { "fields": ["mode"] }     // 조회할 상태 필드 목록
}

상태 설정 — s2r/{robot_id}/state/set

관제가 일부 상태 필드를 설정합니다. 본문은 상태 발행과 동일 구조의 부분 집합을 사용합니다.

상태 발행 — r2s/{robot_id}/state

robot 상태가 변하는 필드값을 이벤트 단위로 로봇이 발행합니다. retain=true 권장. 자발·고빈도 텔레메트리이므로 requestId는 생략 가능하며 occurredAt으로 순서를 판단합니다.

{
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "online": true,
    "mode": "bringup",
    "status": "standby",
    "ems": ["button", "door", "LiDAR", "bumper"],
    "location_score": 0,
    "location": { "x": 0.0, "y": 0.0, "th": 0.0, "map_id": "" },
    "battery": {
      "voltage": 0, "current": 0, "soc": 0,
      "est_charge_complete": 0, "est_discharge_complete": 0,
      "soh": 0, "temperature": 0
    }
  }
}
필드타입단위설명
payload.onlineboolean-로봇 접속 여부 — true(온라인) / false(오프라인). 접속 시 true, 연결 끊김 시 LWT로 false. 관제는 이 값으로 활성(배차 대상) 로봇을 판별합니다(아래 접속 상태 callout).
payload.modeenum-bringup · ready · navi · cruise · delivery · trash_pickup · mapping · charging
payload.statusenum-standby · doing · done (§1 status 어휘 통일 — 종전 paused는 사용하지 않습니다)
payload.emsstring[]-EMS 트리거 소스 (button · door · LiDAR · bumper · remote). remote는 관제가 §3.1 action/set estop으로 발동한 정지입니다 — 로봇 자체 트리거와 구분해 운영자가 원인을 압니다
payload.location_scorenumber0~100위치 추정 신뢰도
payload.location.x / y / thnumber-위치 좌표 및 방향
payload.location.map_idstring-맵 식별자 (§7 맵 등록의 mapId와 동일 값)
payload.location.lat / lng / headingnumber도(°)WGS84 위경도·방위각. 맵 로컬 좌표를 대체하지 않고 병기하며, 값이 없으면 세 키를 생략합니다(0 으로 채우지 않음) — 로봇 발행 항목 §2
payload.battery.voltagenumberV전압
payload.battery.currentnumberA전류
payload.battery.socnumber% (0~100)충전 상태
payload.battery.est_charge_completenumbermin충전 완료 예상 시간
payload.battery.est_discharge_completenumbermin방전 완료 예상 시간
payload.battery.sohnumber% (0~100)배터리 수명
payload.battery.temperaturenumber°C배터리 온도

도어 상태는 미션 컨텍스트(어느 오더의 어느 칸인지)에서 의미가 있으므로 state가 아니라 goals[].mission[].door[]로 발행합니다(§5). ems의 트리거 소스 door(도어로 인한 EMS)와는 별개입니다.

상태 확장 필드(주행 속도·내부 온도·센서 헬스·통신 품질·보유 맵 버전·누적 주행거리·가동 시간·충전 도킹)는 별도 문서 로봇 발행 항목 §2·§5에 정의되어 있으며, 같은 r2s/{robot_id}/state 페이로드에 함께 싣습니다.

부분(delta) 전송 — 상태는 변동 필드만 실어 보내므로 한 페이로드에 모든 키가 오지 않습니다(실측: 위치만, {"online":false}만, mode+status만, 접속 시 전체 스냅샷). 관제는 occurredAt 기준 last-write-wins 로 최신값을 유지·병합합니다.

상태는 이 토픽 하나로 보냅니다 — 변동 주기가 서로 달라도(자주 바뀌는 위치 vs 드물게 바뀌는 battery soh/temperature vs 희소한 mode/status 전이) 토픽을 더 나누지 않습니다. 주기 차이는 위 부분(delta) 전송이 이미 흡수합니다 — 변하지 않은 값은 실리지 않으므로, 자주 바뀌는 값과 드물게 바뀌는 값이 한 토픽에 있어도 서로의 발행량을 끌어올리지 않습니다. 한 토픽으로 두면 동일 시점 상태도 occurredAt 기준으로 그대로 재구성됩니다.

채널을 가르는 기준은 빈도가 아니라 성질입니다 — 최신값만 의미 있어 덮어써도 되는 값(상태)은 이 토픽에 싣고, 발생 자체가 기록이라 놓치면 복구할 수 없는 것(이벤트)은 각자의 토픽(alarm·safety·software·geofence·goals)에 싣습니다.

접속 상태(online)와 LWT

online은 로봇의 MQTT 접속 여부를 나타내며, 관제가 배차 대상(활성) 로봇을 추리는 기준입니다. 로봇은 다음과 같이 관리합니다.

오프라인 감지 지연은 브로커 keepalive 주기에 좌우됩니다(LWT는 keepalive 만료 후 발동). 배차에서 오래된 online: true가 남는 창을 줄이려면 keepalive를 짧게 두는 편이 유리합니다. keepalive 값 협의

4.2 알람 발행 — r2s/{robot_id}/alarm

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "alarm": [
      { "code": "0000", "level": "warn", "msg": "" },
      { "code": "1000", "level": "warn", "msg": "" }
    ]
  }
}
필드타입설명
payload.alarm[].codestring알람 코드
payload.alarm[].levelenumwarn · error · fatal
payload.alarm[].msgstring알람 메시지

알람 리셋 — s2r/{robot_id}/alarm

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": { "alarm": "reset" }      // 알람 리셋 명령
}

알람 코드·레벨별 관제 대응 협의 대기

레벨별 관제 동작 방향(예시)은 아래와 같으며, 실제 코드·레벨 매핑은 양사 시나리오 협의로 확정합니다. 정해지는 대로 관제 대응에 반영합니다.

level관제 대응(예시)리셋
warn로그 저장 및 대시보드 표시자동
error홈 복귀 및 배차 제외 → 운영자 알림자동
fatal즉시 중단 및 배차 제외 → 운영자 확인수동

5. goals info — 미션·입찰

로봇 서비스는 mode → goal → mission 3계층으로 정의합니다. mode는 로봇이 현재 무슨 서비스를 수행 중인지(robot 레벨), goal은 어디로 가는지(목적지 단위), mission은 그 지점에서 무엇을 하는지(동작)를 나타냅니다. 각 계층의 어휘는 서로 겹치지 않게 분리합니다 — 장소 성격은 goal.type, 동작은 mission.type에만 둡니다.

하나의 mission은 하나의 고객 오더 단위이며, 동일 mission.id가 여러 goal에 걸쳐 존재할 수 있습니다 — 예: 픽업 goal과 하차 goal이 같은 오더로 묶입니다. 미션을 로봇에 부여하는 경로는 입찰(goals/offer → bid → result)이며, 낙찰 통보가 곧 수행 개시 신호입니다. 부여 후 로봇은 r2s/{robot_id}/goals로 수행 상태를 발행하고, 관제가 중간에 상태를 바꿔 지시할 때만 goals/set(부분 갱신)을 씁니다. 멀티스톱 배송은 goal 배열로 표현합니다.

계층위치의미값(enum)
modestate.mode로봇 서비스 종류 / 동작 모드bringup · ready · navi · cruise · delivery · trash_pickup · mapping · charging
goal.typegoals[].type목적지(지점)의 성격customer(고객) · store(매장) · unloading(하역장) · charging(충전소) · waiting(대기장소) · none
mission.typegoals[].mission[].type지점에서 수행하는 동작(개방형 — 아래 설명)관제가 지시하는 값: pickup · dropoff · wait · charge
로봇이 세분해 보고하는 값: move · auth · gate_* · elevatorCall · sourceFloor* · destinationFloor* …
status세 계층 공통진행 상태 — 계층별로 어휘가 다르지 않습니다standby · doing · done (취소 canceled) — §1 status 어휘

mission.type은 개방형입니다 — 관제는 목록을 강제하지 않습니다

관제가 goals/offer로 지시하는 미션 타입은 pickup·dropoff(및 wait·charge) 네 가지입니다. 그러나 로봇은 수행 중 이를 자체 단계로 세분해 보고하며(실측: 한 goal 당 20여 단계), 관제는 그 목록을 고정하지 않습니다.

2026-08 실측 어휘: move · map_load · auth · pickup · dropoff · gate_private · gate_public · elevatorCall · sourceFloorCallConfirmed · sourceFloorGetOn · sourceFloorGettingOn · destinationFloorCallConfirmed · destinationFloorGetOff · destinationFloorGettingOff

관제가 의미를 부여하는 단계는 셋뿐입니다 — auth:doing(지점 도착) · pickup:doing/dropoff:doing(박스 열림) · pickup:done/dropoff:done(박스 닫힘·해당 지점 완료). 그 밖의 단계는 done으로 보고되면 이름 그대로 운행 타임라인에 기록되므로, 새 단계가 늘어도 관제 수정 없이 남습니다. 단계 이름을 바꾸면 과거 이력과 어휘가 갈리므로 사전 공유 부탁드립니다.

mode 전이 — 충전 복귀는 navi

배송 로봇은 배차 전 대기·이동, 그리고 다음 배송으로의 연속 배차까지 mode=delivery를 유지합니다(일감 유무·진행은 state.status·goal.status로 표현). 단 충전소 복귀는 delivery가 아니라 mode=navi + goal.type=charging(mission.type=charge)으로 전이합니다 — 관제 지시 또는 다음 배차가 없을 때 로봇이 스스로 복귀하는 timeout 자동 회차. 동작 흐름은 시나리오 §4 참조. timeout 값 협의

충전 복귀 우선순위 — 관제 지시와 로봇 자동 회차

관제 주도 복귀와 로봇 자동 회차는 공존합니다. 관제 지시가 우선이며, 로봇은 수행할 수 없으면 goals/bid로 거절할 수 있습니다. 미션 중 처리(지금 중단할지 완료 후 이동할지)는 로봇이 정합니다.

5.1 goal 본문 — s2r/{robot_id}/goals/offer · goals/set

goal은 mission[] 배열을 품습니다. 이 본문이 goal의 정본 구조이며, 관제가 goal 전체 정의를 실어 보내는 곳은 입찰 요청(goals/offer, §5.3 ①)입니다. 낙찰(goals/result) 통보를 받은 로봇은 offer 로 받아 둔 이 goal 을 그대로 수행합니다.

goals/set은 이미 부여된 goal·mission의 상태를 관제가 바꿔 지시할 때 쓰는 부분 갱신 채널입니다(아래 callout). 아래 예시는 한 오더의 픽업(매장 goal)·하차(고객 goal)가 같은 mission.id로 두 goal에 걸친 모습입니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "goals": [
      {
        "id": "g-01a0324f-eb55-7ba6-a670-91ace2d98ca5-store",
        "type": "store",
        "status": "standby",
        "arrivalOrder": 1,
        "location": { "x": 0.0, "y": 0.0, "th": 0.0, "map_id": "" },
        "mission": [
          {
            "id": "m-01a0324f-eb55-7ba6-a670-91ace2d98ca5",
            "type": "pickup",
            "status": "standby",
            "atomic": true,
            "PIN": "5678",
            "door": [ { "name": "door2", "status": "closed" } ],
            "orderId": "ZWS-ORD-20260903...",
            "address": "",
            "addressDetail": "101동 1503호",
            "phoneNumber": "",
            "message": ""
          }
        ]
      },
      {
        "id": "g-01a0324f-eb55-7ba6-a670-91ace2d98ca5-customer",
        "type": "customer",
        "status": "standby",
        "arrivalOrder": 2,
        "location": { "x": 0.0, "y": 0.0, "th": 0.0, "map_id": "" },
        "mission": [
          {
            "id": "m-01a0324f-eb55-7ba6-a670-91ace2d98ca5",
            "type": "dropoff",
            "status": "standby",
            "atomic": true,
            "PIN": "1234",
            "door": [ { "name": "door2", "status": "closed" } ],
            "orderId": "",
            "address": "",
            "addressDetail": "",
            "phoneNumber": "",
            "message": ""
          }
        ]
      }
    ]
  }
}
필드타입설명
goals[].idstringgoal 식별자 — 관제가 발급합니다. 형식 g-{미션UUID}-{goal.type}. 로봇은 받은 값을 그대로 echo 해야 합니다(아래 callout)
goals[].typeenum지점 성격 — customer · store · unloading · charging · waiting · none
goals[].statusenumstandby · doing · done (취소 canceled) — 도착·해당 지점 완료가 done입니다. §1 status 어휘
goals[].arrivalOrdernumber도착 순서. 관제는 1부터 부여하며, 로봇이 여러 미션을 통합 재계획한 뒤에는 배열 전역으로 1..N 재부여해 보고합니다(미션별로 1 부터 다시 시작하지 않음)
goals[].location.x / y / thnumber위치 좌표 및 방향
goals[].location.map_idstring맵 식별자
goals[].mission[].idstring미션 식별자 (= 고객 오더 단위, 여러 goal에 걸쳐 동일 id 가능). 관제 발급 형식 m-{미션UUID}
goals[].mission[].typestring동작. 관제 지시값은 pickup · dropoff · wait · charge, 로봇 보고값은 개방형 — §5 서두 callout
goals[].mission[].statusenumstandby · doing · done (취소 canceled) — §1 status 어휘
goals[].mission[].atomicboolean미션 원자성 — 묶음 순서 보존 여부 (아래 설명)
goals[].mission[].PINstring | null적재함 인증 PIN (로봇 PIN 입력 방식 시 값, 그 외 null — §6)
goals[].mission[].door[].namestring대상 도어 이름 — 관제가 배정한 적재함 칸 번호를 door{n} 형식으로 지정합니다(door1~door4). n의 상한은 로봇별 칸 수(§6.1)
goals[].mission[].door[].statusenum도어 상태 — opening · opened · closing · closed (로봇이 발행)
goals[].mission[].orderIdstring주문 식별자 — 관제가 발급(ZWS-ORD-…/ZWS-TRA-…). 로봇은 이 값을 해석하지 않고, r2s/{robot_id}/resource 요청 시 그대로 되돌려 보냅니다(§5.5)
goals[].mission[].address / addressDetailstring주소 · 상세 주소
goals[].mission[].phoneNumberstring연락처
goals[].mission[].messagestring메시지
goals[].mission[].autoDropOffboolean비대면 자동 하차 여부. 배차 로봇의 robot.auto_drop_off=true일 때만 true(하차 mode auto). 그 외 false(하차 mode attended, PIN 선전달) — §6

mission.atomic — 묶음 순서 보존

atomic은 한 미션(고객 오더)에 속한 goal들의 순서가 깨지지 않고 묶여서 유지됨을 보장하는 옵션입니다. 이후 다른 미션이 추가되어 거리 최적화로 재정렬되더라도, atomic 미션에 속한 goal들의 상대 순서는 보존되며 다른 미션의 goal이 그 사이에 끼어들지 않습니다.

확정 필요: goal.type: waiting / none 의미, arrivalOrder 동률·미지정 처리, 한 미션에 goal이 다수(예: 5개 이상)일 때 배차(복수 로봇 동시 호출 등) 정책. 협의 대기

goal.id · mission.id 는 관제가 발급하고, 로봇은 그대로 되돌려 보냅니다 중요

goals/set은 부분 갱신입니다 — 전체 goal 을 다시 싣지 않습니다

부여된 goal·mission 의 상태를 관제가 바꿔 지시할 때 씁니다. id + 바뀐 필드만 실으며, 싣지 않은 필드는 "변경 없음"입니다(null로 덮어쓰지 않습니다).

// s2r/{robot_id}/goals/set — 매장 인증 단계 완료 지시(관제 문열림 처리 시)
{
  "requestId": "uuid",
  "occurredAt": "2026-09-03T14:00:00+09:00",
  "payload": {
    "goals": [
      { "id": "g-01a0324f-…-store", "mission": [ { "id": "…auth 단계 id…", "status": "done" } ] }
    ]
  }
}

현재 관제가 실제로 발행하는 goals/set은 이 한 가지(매장 인증 단계 done 지시)입니다. id는 로봇이 보고한 값을 그대로 되돌려 싣습니다.

5.2 goal 수행 발행 — r2s/{robot_id}/goals

수행 중 goal 상태(진행률·도착예상·상태)를 로봇이 발행합니다. retain=true. 일감이 없으면 goals: []로 보고합니다.

이 배열은 로봇이 들고 있는 미션 전체의 스냅샷입니다 — 동시에 여러 미션을 낙찰받으면 배열 하나에 모두 실어 보냅니다(실측 최대 4미션·goal 8개). 관제는 goal.id 접두로 미션을 가르므로 미션별로 토픽·메시지를 나눌 필요가 없습니다. 동시에 doing인 goal 은 하나이고 나머지는 standby로 줄 세워 둡니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "goals": [
      {
        "id": "g-01a0324f-…-store",
        "type": "store",
        "status": "doing",
        "arrivalOrder": 1,
        "estArrival": "2026-06-04T10:07:00+09:00",
        "progress": 0,
        "location": { "x": 0.0, "y": 0.0, "th": 0.0, "map_id": "" },
        "mission": [
          { "id": "…move 단계 id…", "type": "move",   "status": "done",  "atomic": true, "orderId": "ZWS-ORD-…" },
          { "id": "…auth 단계 id…", "type": "auth",   "status": "doing", "atomic": true, "orderId": "ZWS-ORD-…" },
          { "id": "m-01a0324f-…",   "type": "pickup", "status": "standby", "atomic": true, "orderId": "ZWS-ORD-…" }
        ]
      }
    ]
  }
}
필드타입단위설명
goals[].estArrivalstringISO-8601도착 예상 시각 — occurredAt과 동일 포맷(예 2026-08-21T14:03:00+09:00)
goals[].progressnumber0~100진행률
goals[].statusenum-standby · doing · done (취소 canceled + reason) — §1 status 어휘
goals[].mission[].*--goal 본문(§5.1)의 mission 필드와 동일. 진행 보고에서는 id·type·status·orderId만 실어도 됩니다

도착 예상(estArrival) 규약 — 절대 시각 2026-08-21 변경

estArrival은 "남은 분(정수)"이 아니라 도착 예상 시각(절대 시각)입니다. occurredAt과 동일한 ISO-8601 오프셋 포맷(초 단위, 예 2026-08-21T14:03:00+09:00)으로 싣습니다. 관제는 같은 메시지의 estArrival − occurredAt으로 남은 시간을 환산하며(둘 다 로봇 시계라 시계 오차가 상쇄됨), 하차지 도착 5분 전 고객 알림도 이 환산값으로 판정합니다.

값이 변하지 않는 구간에서는 발행하지 않아도 됩니다. 로봇 생존 여부는 state 발행으로 판단하므로 goals 침묵은 이상 신호로 보지 않습니다. 전환기 하위호환: 관제는 종전 정수(분) 값도 계속 수용합니다(로봇은 절대 시각으로 전환).

5.3 입찰 (offer → bid → result)

입찰은 3단계입니다. 관제가 후보 로봇에 offer로 제시 → 각 로봇이 bid로 goal별 도착 예상 시각 응답 → 관제가 전체 ETA(마지막 goal 도착 예상 시각 기준 남은 시간) 최소 로봇으로 배차를 결정(추후 적재공간 기준 포함)해 result로 낙찰 여부를 통보합니다. 어느 로봇이 수행할지의 최종 배차 결정은 관제가 단독으로 가집니다(배차 확정·충돌 방지).

입찰 라운드 마감

입찰·낙찰은 goalId 단위로 식별합니다(별도 오퍼 식별자 없음). 라운드는 단발이며 재오퍼하지 않습니다 — bid 타임아웃 시점에 관제가 모인 입찰로 배차를 결정하고, 마감 후 도착한 지각 bid는 배제합니다. (로봇, goal)당 in-flight 오퍼는 1건입니다. 입찰 가능한 로봇이 없으면 해당 미션은 수행 불가로 종료됩니다. 타임아웃 값 협의

offer 대상과 타임아웃

① 입찰 요청 — s2r/{robot_id}/goals/offer

goal 전체 정의(§5.1 본문)를 그대로 실어 보냅니다 — id·type·status·arrivalOrder·location과 mission[](PIN·door·orderId·addressDetail·autoDropOff 포함). 낙찰 로봇은 이 본문을 그대로 수행하며, 별도의 goal 재전송은 없습니다.

따라서 goals/offer는 "입찰 요청"인 동시에 조건부 goal 부여입니다. 미낙찰 로봇은 result{claimed:false}를 받고 해당 goal 을 폐기합니다.

② 입찰 응답 — r2s/{robot_id}/goals/bid

quote/bid(§5.4 ②)와 동일한 스키마입니다 — 두 응답은 필드 구성이 같고, 쓰이는 맥락만 다릅니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "goals": [
      { "id": "g-01a0324f-…-store",    "arrivalOrder": 1, "available": true, "estArrival": "2026-06-04T10:05:00+09:00", "reason": null },
      { "id": "g-01a0324f-…-customer", "arrivalOrder": 2, "available": true, "estArrival": "2026-06-04T10:12:00+09:00", "reason": null }
    ]
  }
}

offer 로 받은 goal 전부에 대해 응답합니다. 수행할 수 없으면 available=false·estArrival=null·reason(사유)으로 답합니다. 입찰하지 않겠다면 응답하지 않아도 됩니다(타임아웃으로 무응답 처리).

관제는 available=false 입찰을 낙찰하지 않습니다 2026-09-03 반영

아카이브 실측(2026-08-04~09-03)을 goal 단위로 집계한 결과입니다 — 로봇·미션 단위 입찰 489건 중 81건이 전 goal 불가였고, 입찰 로봇 전원이 불가인 라운드가 41건이었습니다. 그 41건 중 완주한 미션은 없습니다(미배정 28 · 중단 13). 즉 불가 응답이 나온 라운드는 사실상 전부 실패로 끝나고 있었고, 이 변경은 그 실패를 배차 시점에 사유와 함께 드러냅니다.

정정(2026-09-04) — 이 문단의 최초 판에는 "goals/bid 473건 중 82건 불가 · 라운드의 16%가 전원 불가"로 적혀 있었고 "종전에는 불가라고 답한 로봇에게도 배차가 나갔다"고 서술했습니다. 앞의 수치는 메시지 단위로 집계해 다중 미션 번들을 한 미션으로 귀속시킨 오류였고, 뒤의 서술은 특정 시기에만 해당해 현재 동작으로 읽히면 사실과 다릅니다. 위 문단이 정정본입니다.

실측 reason 값은 전량 "no feasible path" 였습니다. 관제는 이 값을 로그·감사에만 쓰고 의미로 분기하지 않으므로 자유 문자열로 두어도 됩니다. 다만 사유별 재시도 정책을 두려면 코드 집합을 함께 확정해야 합니다. 협의 대상

③ 입찰 결과 — s2r/{robot_id}/goals/result

offer 로 제시한 goal 전부에 낙찰 여부를 실어 보냅니다. 순서는 offer 와 동일합니다. 낙찰(claimed:true) 통보가 곧 주행 개시 신호이며, 관제는 이후 별도의 출발 지시(state/set 등)를 보내지 않습니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "goals": [
      { "id": "g-01a0324f-…-store",    "claimed": true, "reason": null },
      { "id": "g-01a0324f-…-customer", "claimed": true, "reason": null }
    ]
  }
}
필드타입설명
goals[].idstringoffer 의 goal id — 관제 발급값 그대로 bid · result
goals[].arrivalOrdernumber도착 순서 bid
goals[].availableboolean수행 가능 여부 bid
goals[].estArrivalstring | null도착 예상 시각(ISO-8601, occurredAt과 동일 포맷 — §5.2 규약 참조). 불가 시 null bid
goals[].reasonstring | nullbid: 불가 사유(available=false 시) · result: 미낙찰 사유(현재 outbid) bid · result
goals[].claimedboolean낙찰 여부 result

result는 bid 를 보낸 로봇에게만 발송됩니다. 무응답 로봇은 통보를 받지 않으므로, offer 를 받고 응답하지 않았다면 해당 goal 은 스스로 폐기해야 합니다.

5.4 배송 가능여부 견적 (quote) 운영 적용됨

주문이 확정되기 전, 관제가 후보 로봇에게 "지금 이 배송을 수행할 수 있는지"를 미리 물어 가용성·예상 도착시간을 받아 두는 사전 견적입니다. 미션(goal)을 생성하지 않는 비구속 질의이므로, 배차 입찰(§5.3 goals/*)과 토픽을 분리해 quote/* 네임스페이스로 운영합니다. 낙찰(result) 단계가 없어 입찰의 3단계를 2단계(질의 → 견적)로 축약하며, 응답(bid) 스키마는 배차 입찰과 완전히 동일합니다. 관제는 모든 goal 이 available인 로봇 중 전체 ETA(마지막 goal 도착 예상 시각 기준 남은 시간) 최소를 가용 ETA로 집계합니다(배차 낙찰과 동일 방식).

입찰(goals)과의 차이 — 응답은 같고, 질의만 다릅니다

배차 입찰 goals/*견적 quote/*
질의(offer) 본문goal 전체 정의(§5.1) — id·PIN·door·orderId 포함축약형 — quoteId·deliveryType·목적지 요약(아래 ①)
응답(bid) 본문동일 스키마 — goals[]{ id, arrivalOrder, available, estArrival, reason }
단계3단계 (offer → bid → result)2단계 (offer → bid, 낙찰 없음)
미션 생성낙찰 시 생성 · 수행 개시생성하지 않음(비구속 질의)
응답 상관goal id토픽의 robot_id (견적 응답에 quoteId를 싣지 않습니다)

① 견적 질의 — s2r/{robot_id}/quote/offer

견적 단계에는 아직 goal 이 없으므로 goal 전체 정의가 아니라 축약 본문을 씁니다. 로봇은 목적지 좌표와 필요한 적재 칸 수만으로 "지금 이 배송을 맡을 수 있는지"를 판정합니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-07-05T10:00:00+09:00",
  "payload": {
    "quoteId": "01a03250-e223-7f8e-8d2c-9eb06b4fdfa4",
    "deliveryType": "delivery",                 // delivery | trash_pickup
    "goals": [
      { "type": "store",    "location": { "x": 12.3, "y": 45.6, "th": 0.0, "map_id": "ground" } },
      { "type": "customer", "location": { "x": 78.9, "y": 10.1, "th": 0.0, "map_id": "ground" }, "boxCount": 2 }
    ]
  }
}
필드타입설명
quoteIdstring견적 라운드 식별자(UUID v7). 응답에는 싣지 않습니다 — 상관은 토픽 robot_id로 합니다
deliveryTypeenumdelivery(배송) · trash_pickup(수거). 목적지 순서가 달라집니다 — 배송 매장→세대, 수거 세대→수거장
goals[].typeenum목적지 성격 — goal.type과 같은 어휘(store · customer · unloading)
goals[].locationobject목적지 좌표(§5.1과 동일 구조). 노드 좌표를 못 찾으면 생략됩니다
goals[].boxCountnumber필요한 적재함 칸 수. 상차가 일어나는 세대(customer) goal 에만 싣습니다

배차 입찰과 달리 goal id·PIN·주문번호가 없습니다 — 아직 주문이 확정되지 않은 단계이기 때문입니다.

② 견적 응답 — r2s/{robot_id}/quote/bid

배차 입찰 응답(goals/bid, §5.3 ②)과 동일한 스키마입니다. 불가 시 available=false·estArrival=null·reason(사유)로 응답합니다. 관제는 모든 goal 이 available일 때만 가용으로 보고 마지막 goal 도착 예상 시각 기준 남은 시간으로 예상 도착시간을 산출합니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-07-05T10:00:03+09:00",
  "payload": {
    "goals": [
      { "id": "", "arrivalOrder": 1, "available": true, "estArrival": "2026-07-05T10:07:00+09:00", "reason": null },
      { "id": "", "arrivalOrder": 2, "available": true, "estArrival": "2026-07-05T10:14:00+09:00", "reason": null }
    ]
  }
}
필드타입단위설명
goals[].idstring-견적에는 goal id 가 없으므로 빈 문자열로 둡니다. 상관은 토픽 robot_id로 합니다
goals[].arrivalOrdernumber-질의의 goals[] 순서에 대응하는 도착 순서
goals[].availableboolean-수행 가능 여부
goals[].estArrivalstring | nullISO-8601도착 예상 시각(occurredAt과 동일 포맷 — §5.2 규약 참조, 불가 시 null)
goals[].reasonstring | null-불가 사유 (available=false 시)

적재함 만재는 관제가 판정합니다 — 로봇이 칸 점유를 알 수 없으므로, boxCount만큼 칸을 배정할 수 없는 로봇은 관제가 채택 단계에서 제외합니다. 로봇은 주행 가능 여부만 판단해 답하면 됩니다.

라운드 마감 · 무응답 처리

입찰과 동일하게 단발 라운드(재질의 없음)이며, bid 타임아웃도 배차와 동일(초기값 3초)입니다. 타임아웃 내 무응답 로봇은 available=false로 간주하고 지각 응답은 배제합니다. 후보 전원이 unavailable·무응답이면 가용성 결과는 수행 가능 로봇 없음으로 집계됩니다. 상관(어느 라운드 응답인지)은 토픽의 robot_id로 판별합니다 — 견적 응답에는 별도 견적 식별자가 없습니다. 운용 중 조정

5.5 인프라 자원 요청 — 엘베·로비폰 운영 적용됨

로봇이 미션 수행 중 엘리베이터·로비폰(공동현관)이 필요한 시점에 관제로 요청합니다. 주소 정보는 관제가 orderId 로 보유합니다. 단, 엘리베이터는 로봇이 도착한 엘베 코드(elevatorCode)와 탑승층·목적층(sourceFloor·destinationFloor)을 함께 전달합니다 — 관제가 elevatorCode로 해당 엘베 라인(홀)의 호출 식별자를 조회해 MIRI 콜에 사용합니다. 실제 탑승 호기는 로봇이 고르지 않고 엘베 그룹제어가 배정하며, 관제는 배정된 호기(elId)를 resource/status 로 로봇에 전달해 로봇이 그 호기 문 앞으로 이동하도록 안내합니다. 로비폰은 로봇이 개방 대상 lobbyId(예: GOCHEOK-101)를 함께 전달하면, 관제가 lobbyId로 DB(lobbyphone 레지스트리)를 조회해 해당 로비의 siteId·IP·개방시간을 얻어 자동 개방(inBase 호출)합니다 — 로봇은 inBase·IP·토큰을 몰라도 됩니다. 처리 완료 시 관제가 진행 신호를 보내고, 로봇은 다음 단계로 이동합니다.

// 로봇 → 관제 : r2s/{robot_id}/resource  (엘베)
{ "orderId": "ZWS-ORD-...", "resource": "elevator",
  "elevatorCode": "EL-S002506-103", "sourceFloor": "3", "destinationFloor": "15",
  "building": "101" }                       // 선택 — 동 표기(있으면 감사 기록에 사용)

// 로봇 → 관제 : r2s/{robot_id}/resource  (로비폰)
{ "orderId": "ZWS-TRA-...", "resource": "lobbyphone", "lobbyId": "GOCHEOK-101" }

// 관제 → 로봇 : s2r/{robot_id}/resource/ready
//   result(로비폰): opened | failed   /  result(엘베): ok | canceled(취소 성공, 종결 신호) | failed(취소 거부)
{ "orderId": "ORD-...", "resource": "lobbyphone", "result": "opened" }
{ "orderId": "ORD-...", "resource": "elevator",   "result": "ok" }

orderId는 goal 의 mission[].orderId를 그대로 되돌려 줘야 함

관제는 orderId로 어느 미션의 요청인지 찾아 엘베 콜 감사 원장·운행 타임라인을 기록합니다. 이 값이 로봇 내부 스텝 id(예: g-…-store_01_elevatorCall)로 오면 미션을 못 찾아 이력이 통째로 비어 남습니다 — 엘베 호출·주행 자체는 정상 동작하므로 겉으로는 드러나지 않습니다.

탑승·하차 진행 보고에서도 같은 값을 써야 함(요청과 보고의 orderId 네임스페이스가 갈리는 사례가 관측됨).

요청 순서는 로비폰(건물 진입) → 엘베(상층 이동)입니다. 엘베는 로봇이 전달한 elevatorCode 로 관제가 호출 식별자를 조회하고, 로봇이 준 sourceFloor·destinationFloor 로 호출·탑승·하차를 조율하며, 하차 완료 후 진행 신호를 보냅니다. 로비폰은 자동 개방(입주민 호출 없음)이며, 로봇이 보낸 lobbyId 는 관제 DB(lobbyphone.lobby_id, status=active)의 값과 글자까지 정확히 일치해야 합니다 — 불일치·미등록이면 관제가 failed 를 회신합니다(신규 동/단지는 관제 DB에 로비 레코드를 먼저 등록하고 lobbyId 를 공유). 관제가 실제 개방 상태(opened/failed)를 result 로 회신하며, 로봇은 opened 일 때만 진입하고 그 외엔 대기·재시도합니다. 문 열림 시간은 관제 설정값(레코드별 durationSec)이며 로봇이 지정하지 않습니다.

엘베 중간 상태(탑승·하차)

엘베는 호출 후 탑승·하차 단계가 진행됩니다. 관제는 MIRI serviceStatus(호출확정→탑승→하차완료, 24종)를 받아 로봇에 중계(resource/status)하고, 로봇은 실제 탑승·하차 시점에 보고하면 관제가 이를 MIRI thing/status 로 중계합니다. 배정 호기는 콜 배정 확정(sourceFloorCallConfirmed) 시점부터 elId 로 함께 실려 오므로, 로봇은 그 호기 앞에서 탑승을 준비합니다.

// 관제 → 로봇 : s2r/{robot_id}/resource/status  (중간 상태 중계)
{ "resource": "elevator",
  "serviceStatus": "sourceFloorCallConfirmed" | "sourceFloorGetOn" | "sourceFloorGetOnCheck"
                 | "destinationFloorGetOff" | "destinationFloorGetOffCheck" | …,
  "elId": "EL-..." }

// 로봇 → 관제 : r2s/{robot_id}/resource  (탑승/하차 보고 → 관제가 MIRI thing/status 중계)
//   status 어휘는 MIRI serviceStatus 를 그대로 쓰는 것이 정본입니다(실 로봇 발행값).
{ "orderId": "ZWS-ORD-...", "resource": "elevator",
  "status": "sourceFloorWaiting" | "sourceFloorGettingOn" | "sourceFloorGotOn"
          | "destinationFloorGettingOff" | "destinationFloorGotOff" }
//   status 취소값(탑승 진행 어휘와 별개, §5.5): "cancelByRobot" — 탑승 실패로 로봇이 콜을 포기할 때

탑승 완료 = sourceFloorGotOn, 하차 완료 = destinationFloorGotOff 입니다. 이 두 전이는 MIRI 가 push 하지 않아 로봇 보고가 유일한 소식통입니다 — 누락되면 콜이 취소될 뿐 아니라 완료 이력도 남지 않습니다.

관제는 콜 배정 확정(sourceFloorCallConfirmed) 시점에 배정 호기(elId)를 실어 보내며, 로봇은 그 호기 앞에서 대기합니다. 이어 로봇은 sourceFloorGetOn(탑승 안내) 시 탑승 후 status:"sourceFloorGotOn", destinationFloorGetOff(하차 안내) 시 하차 후 status:"destinationFloorGotOff" 를 보고합니다. serviceStatus 어휘와 배정 호기 식별자(elId)는 MIRI 표준값을 그대로 사용하며, 호기 식별자→실제 문 위치 매핑은 제로웍스와 협의해 확정합니다.

탑승·하차 완료 보고는 필수입니다 — 누락 시 콜이 취소됩니다

status:"sourceFloorGotOn"(탑승 완료)·status:"destinationFloorGotOff"(하차 완료) 는 선택 보고가 아니라 시퀀스를 다음 단계로 넘기는 신호입니다. 엘리베이터는 이 완료 보고를 받아야 도어를 닫고 이동합니다.

완료 보고가 오지 않으면 엘리베이터는 도어 닫힘 전 재확인을 보냅니다 — 탑승은 sourceFloorGetOnCheck, 하차는 destinationFloorGetOffCheck 이며, 각각 안내(…GetOn/…GetOff) 이후 25초가 지나면 10초 간격으로 최대 3회 발생합니다. 관제는 이 재확인도 resource/status 로 그대로 중계합니다.

재확인에도 완료 보고가 없으면 엘리베이터가 트랜잭션을 취소합니다(cancelByDoorTime·cancelByDeadline 등). 취소되면 그 콜은 되살릴 수 없고 처음부터 다시 호출해야 합니다. 실제 연동 시험에서 sourceFloorGettingOn(탑승 중)까지만 보고하고 탑승 완료(sourceFloorGotOn)를 보내지 않아, 재확인 3회 뒤 약 3~4분 만에 콜이 취소된 사례가 확인되었습니다.

정리하면 출발층은 sourceFloorGetOn(관제→로봇, 탑승 요청) → sourceFloorGotOn(로봇→관제, 탑승 완료), 목적층은 destinationFloorGetOff(하차 요청) → destinationFloorGotOff(하차 완료) 가 한 쌍입니다. 요청을 받으면 반드시 완료로 답해야 합니다.

엘베 콜 취소 — 로봇이 탑승하지 못해 콜을 포기할 때 같은 resource 토픽에 status: "cancelByRobot" 을 실어 보냅니다. 관제가 MIRI 사물 콜 취소를 대신 수행하고 resource/ready 로 result: "canceled"(성공) 또는 "failed"(MIRI 거부)를 돌려줍니다. canceled 는 진행 신호가 아니라 종결 신호입니다 — ok 와 혼동하지 마십시오. 취소 후 같은 엘베를 다시 부르려면 5초 이상 띄워야 합니다(현장 안정화 단계).

// 로봇 → 관제 : r2s/{robot_id}/resource
{ "orderId": "...", "resource": "elevator", "status": "cancelByRobot" }

// 관제 → 로봇 : s2r/{robot_id}/resource/ready
{ "orderId": "...", "resource": "elevator", "result": "canceled" }

6. PIN·박스 개방

고객 수령(하차) 방식은 로봇 H/W의 비대면 지원 여부(robot.auto_drop_off)로 결정되며, 배차/입찰 시점에 goal.mission으로 전달됩니다. 비스캣은 대면 배송에서 항상 userPin(4자리)을 발행하고 오토메타 배차 응답에 동봉합니다.

하차 modePINautoDropOff동작
비대면 (auto) — 로봇 auto_drop_off=true H/W 한정nulltrue도착 즉시 박스 자동 개방·하차 후 복귀 (고객 조작 불필요)
대면 (attended) — 기본랜덤(4자리)false① 로봇 키패드 PIN 입력(로컬 검증) 또는 ② 오토메타 웹 개방(관제→로봇 문열림 지시)

오토메타 웹 개방용 문열림 지시(s2r)의 토픽·페이로드는 아래 6.1에 있습니다. 상차·하차 완료 보고는 별도 토픽 없이 r2s/{robot_id}/goals의 pickup:done·dropoff:done으로 일원화되었습니다(§6.1). 로봇 키패드 PIN 입력 경로의 PIN 감사 이벤트(r2s) 토픽·페이로드는 여전히 별도 확정 대상입니다. 토픽·페이로드 협의

6.1 박스 개방 지시 — s2r/{robot_id}/box/open

box/loaded · box/unloaded 는 폐지되었습니다 2026-09-03 개정

상차·하차 완료 보고 전용 토픽을 두지 않고 r2s/{robot_id}/goals 한 곳으로 일원화했습니다. 관제는 box/loaded·box/unloaded를 구독하지 않습니다.

사건종전(폐지)현재
박스 열림—mission.type=pickup|dropoff · status=doing
상차 완료(닫힘) = 배송 출발 트리거r2s box/loaded매장 goal 의 pickup:done
하차 완료(수령) = 미션 종료r2s box/unloaded세대 goal 의 dropoff:done
미션 종료 판정—해당 미션의 모든 goal·mission 이 done

보고 창구를 하나로 모아 "박스는 닫혔는데 goal 은 아직 진행 중" 같은 두 원장의 어긋남을 없앴습니다. 개방 지시(box/open)만 별도 토픽으로 남습니다.

개방 지시는 관제가 박스 번호(boxSlots)를 지정해 내려보냅니다. 로봇은 주문번호를 해석하지 않습니다 — 관제가 주문번호↔박스 번호 매핑을 관장하고, 로봇은 지정된 물리 칸만 엽니다. 이 메시지는 행위를 트리거하는 명령이므로 §1 규약대로 requestId(멱등)를 유지하고 QoS 1로 전송합니다. 개방 지시는 로봇이 해당 지점에 도착한 뒤(점포=상차, 세대=하차)에만 유효합니다.

개방 세 경로와의 관계 (§6)

즉 box/open(개방 지시)은 웹 개방(대면 ②)에서만 발행됩니다. 반면 상차·하차 완료 보고는 세 경로 공통으로, 개방 방식과 무관하게 닫힘 시점에 r2s/{robot_id}/goals의 pickup:done·dropoff:done으로 항상 발행됩니다.

① 개방 지시 본문

관제가 열 박스 번호(boxSlots)를 지정해 지시합니다. 관제가 주문번호에 매핑한 번호를 내려주며, 로봇은 지정된 물리 박스 칸만 개방하고 주문번호는 해석하지 않습니다. 상차(점포)·하차(세대) 공용입니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-07-19T14:00:00+09:00",
  "payload": {
    "boxSlots": [1, 3],                  // 관제가 지정한 개방 박스 번호
    "orderId": "ZWS-ORD-..."             // 선택 — 상관용. 로봇은 해석하지 않습니다
  }
}
필드타입설명
boxSlotsnumber[]개방할 박스 번호 배열. 관제가 주문번호에 매핑해 지정 — 로봇은 해당 물리 칸만 개방. 번호 범위는 1..N이며 N은 로봇별 칸 수(관제에 로봇마다 등록, 하드웨어 상한 4)
orderIdstring | null상관용 불투명 토큰(선택). 배차 경로의 개방 지시에서만 채워지며, 로봇은 해석하지 않고 무시해도 됩니다

② 개방·닫힘 결과 보고

별도 토픽 없이 r2s/{robot_id}/goals(§5.2)로 보고합니다.

개방에 실패한 경우(기구 오류 등)는 goal·mission 을 canceled + reason: "door-not-opened"로 보고합니다(로봇 발행 항목 §6).

확정 필요 항목

7. 맵 등록 (REST)

로봇이 보유한 지도와 의미 거점 노드(관제가 goal을 내릴 때 참조하는 명명된 지점)를 siteId 기준으로 관제에 등록합니다. 로봇 UI의 업로드 버튼을 누르면 로봇이 보유 맵을 자동 업로드합니다. 흐름은 등록 개시 → 파일 업로드 → 등록 확정 3단계로, 등록 개시 시 siteId·제시 mapId와 업로드할 파일 목록을 선언하면 관제가 파일별 업로드 URL을 발급하고, 로봇은 지도 이미지·메타(yml)·노드(json) 파일을 각각 해당 URL로 전송합니다(압축하지 않음).

맵 버전 정책

mapId는 로봇(제로웍스)이 제시하며, MQTT 상태·goal의 location.map_id와 동일 값입니다. (siteId, mapId) 조합당 mapVersion이 증가하며 이력을 보관합니다. 등록 확정 시 최신본이 활성 맵이 되고 이전 버전은 비활성으로 전환됩니다(롤백·감사 가능). 관제는 등록된 노드 좌표를 goal의 location으로 사용합니다.

① 등록 개시 — POST /v1/robot/maps

인증: HMAC(로봇). siteId·제시 mapId와 함께 업로드할 파일 목록(files)을 선언하면, 관제가 mapVersion과 선언한 파일별 업로드 URL을 발급합니다. 지도 메타·노드는 본문에 싣지 않고 ②에서 각 파일로 업로드합니다.

인증HMAC (로봇)
요청
{
  "siteId": "APT-1023",
  "mapId": "zw-map-001",        // 로봇(제로웍스)이 제시하는 맵 식별자
  "files": [                    // 업로드할 파일 목록 선언
    { "name": "map.pgm",    "type": "image" },
    { "name": "map.yml",    "type": "meta"  },
    { "name": "nodes.json", "type": "nodes" }
  ]
}
200 응답
{
  "result": "SUCCESS",
  "data": {
    "mapId": "zw-map-001",
    "mapVersion": 3,
    "uploadExpiresIn": 600,
    "uploads": [                 // 선언한 파일별 업로드 URL
      { "name": "map.pgm",    "uploadUrl": "https://...(map.pgm 업로드 URL)..." },
      { "name": "map.yml",    "uploadUrl": "https://...(map.yml 업로드 URL)..." },
      { "name": "nodes.json", "uploadUrl": "https://...(nodes.json 업로드 URL)..." }
    ]
  }
}
에러404 SITE_NOT_REGISTERED
필드타입설명
siteIdstring단지·현장 식별자 (맵 매핑 기준)
mapIdstring로봇(제로웍스)이 제시하는 맵 식별자. MQTT location.map_id와 동일 값을 사용
files[].namestring업로드할 파일명
files[].typeenumimage(지도 이미지) · meta(yml) · nodes(json)

② 파일 업로드 — {uploadUrl}

로봇이 ① 응답의 uploads[]에 따라 각 파일을 해당 업로드 URL에 그대로 PUT합니다(압축하지 않음). uploadExpiresIn 내에 완료해야 합니다.

파일(type)내용
imageoccupancy grid 등 지도 이미지 (pgm/png 등)
meta (yml)지도 메타데이터 — 해상도(m/px)·원점(x/y/th)·크기 등
nodes (json)주행 그래프 노드 목록. 그중 Place 객체를 가진 노드가 목적지(관제가 goal 좌표로 사용) — 아래 포맷 참조

nodes(json) 포맷 — Place 를 가진 노드가 목적지입니다 2026-08-24 확정 반영

{
  "header": { "version": 5, "hash": "…" },
  "nodes": [
    {
      "idx": 12,                                  // ★ 파일 내 노드 식별자 — 관제의 place_id 가 됩니다
      "x": 12.34, "y": 56.78, "th": 0.0,
      "floor": 15,                                // 노드가 속한 건물 층(숫자). 없는 포맷도 허용
      "edge_to": [11, 13],
      "Place": {                                  // 이 객체가 있는 노드만 목적지로 등록됩니다
        "name": "1503호",
        "type": "home",
        "sort_order": 12,
        "size": 1
      }
    }
  ]
}
필드필수설명
nodes[].x · y필수SLAM 좌표. 하나라도 없으면 MAP_VALIDATION_FAILED로 거절합니다
nodes[].idx필수관제 목적지 키 place_id의 원천입니다(전역키 = {mapId, place_id}). idx가 없으면 배열 순번으로 대체되는데, 그러면 다음 등록에서 노드가 추가·삭제될 때 place_id 가 통째로 밀려 기존 주소 매핑이 다른 세대를 가리킵니다.
nodes[].floor권장노드가 속한 층. SLAM 이미지는 2D 평면 한 장인데 노드는 여러 층에 걸쳐 있어, 이 값이 없으면 층별 세대가 같은 좌표에 겹칩니다
nodes[].Place.name필수목적지 이름(예: 1503호·매장명). 관제 주소 매핑 화면에 그대로 노출됩니다
nodes[].Place.type권장목적지 성격. 미지정 시 none
nodes[].Place.sort_order선택표시 순서
nodes[].edge_to선택인접 노드 idx 목록(관제 미사용, 로봇 주행 그래프)

구포맷(소문자 place + 명시 id·place_idx)도 계속 받으며, 명시 id가 있으면 그 값이 place_id로 우선합니다.

선언 목록(files)에 없는 파일은 DB에 반영되지 않고 삭제됩니다. type 별로 파일은 1개씩이어야 하며, 확장자로 type 을 추론합니다(pgm·png → image, yml → meta, json → nodes). meta(yml)는 resolution·origin 키가 있어야 통과합니다.

③ 등록 확정 — POST /v1/robot/maps/{mapId}/commit

업로드 완료를 통보합니다. 관제가 ① 선언 목록의 모든 파일이 업로드됐는지 확인하고, 각 파일을 검증(파싱·필수 항목 확인)해 통과하면 활성화(이전 버전 비활성)합니다. 선언 파일이 하나라도 누락됐거나 검증에 실패하면 거절하며, 선언에 없는 파일은 반영하지 않고 삭제합니다.

인증HMAC (로봇)
200 응답
{
  "result": "SUCCESS",
  "data": {
    "mapId": "zw-map-001",
    "mapVersion": 3,
    "isActive": true,
    "activatedAt": "2026-06-12T10:00:00+09:00"
  }
}
에러422 MAP_UPLOAD_INCOMPLETE(선언 파일 누락), 422 MAP_VALIDATION_FAILED(파일 검증 실패), 409 MAP_UPLOAD_EXPIRED(업로드 URL 만료 후 commit)

8. 지오펜스 (REST · MQTT)

관제 콘솔에서 정의한 다각형 영역을 로봇이 내려받아 보유하고, 경로 계획·주행 중 스스로 판정합니다. 버전 알림은 MQTT, 본문 조회는 REST입니다. 동작 흐름은 시나리오 §9, 로봇이 발행하는 항목은 로봇 발행 항목 §9를 따릅니다.

지오펜스 버전 정책

버전은 단지(siteId) 단위로 매겨지며 영역 건별 버전은 없습니다. 영역의 등록·수정·삭제·활성 변경 시마다 증가합니다. 로봇은 보유 버전과 일치하지 않을 때 내려받습니다 — 수신 버전이 보유 버전보다 작은 경우(되돌림 배포)에도 내려받아야 합니다.

① 버전 알림 — s2r/{robot_id}/geofence/version

단지 지오펜스 버전이 바뀌면 그 단지의 모든 로봇 토픽에 발행합니다. retain이므로 재접속한 로봇은 마지막 버전을 자동으로 수신합니다.

QoS · retain1 · true
본문
{
  "occurredAt": "2026-09-22T10:00:00+09:00",
  "payload": {
    "siteId": "APT-GOCHEOK-001",
    "version": 42
  }
}
§1 의 공통 envelope 을 따릅니다. 발행 시각은 occurredAt 입니다.

② 영역 조회 — GET /v1/robot/geofences

인증: HMAC(로봇) — 맵 등록(§7)과 동일 방식입니다. 로봇이 소속된 단지의 활성 영역 전체를 반환합니다.

인증HMAC (로봇)
요청본문 없음. 대상 단지는 로봇 식별자로 결정됩니다.
200 응답
{
  "result": "SUCCESS",
  "data": {
    "siteId": "APT-GOCHEOK-001",
    "version": 42,
    "geofences": [
      {
        "id": "GF-01a0c3ef",
        "kind": "no-go",           // no-go · avoid · hazard
        "env": "indoor",           // indoor · outdoor
        "mapId": "ground",         // indoor 에만 포함 — location.map_id 와 동일 값
        "vertices": [              // 나열 순서가 변의 순서. 3개 이상
          { "x": 46.3, "y": 193.9 },
          { "x": 52.3, "y": 193.9 },
          { "x": 52.3, "y": 199.9 },
          { "x": 46.3, "y": 199.9 }
        ],
        "effectiveFrom": null,     // null = 제한 없음
        "effectiveTo": null        // null = 무기한
      },
      {
        "id": "GF-01a0c318",
        "kind": "hazard",
        "env": "outdoor",
        "vertices": [              // outdoor 는 WGS84 위경도
          { "lat": 37.4983392, "lng": 126.8599869 },
          { "lat": 37.4978070, "lng": 126.8595129 },
          { "lat": 37.4971505, "lng": 126.8605206 }
        ],
        "effectiveFrom": "2026-09-22T09:00:00+09:00",
        "effectiveTo": "2026-09-30T18:00:00+09:00"
      }
    ]
  }
}
에러401 UNAUTHORIZED(서명 검증 실패), 404 ROBOT_NOT_REGISTERED(미등록 로봇), 404 SITE_NOT_REGISTERED(로봇에 단지 미배정)

③ 적용 회신 · 우회·차단 발행

적용 버전 회신(state.geofence)과 우회·차단 이벤트(r2s/{robot_id}/geofence)의 필드 규격은 로봇 발행 항목 §9에 있습니다.

9. 추후 범위 (setting · remote) 추후 추가 예정

아래 항목은 1.5차 PoC 범위 밖이며 토픽 자리만 예약합니다. (지도 파일 등록은 §7 REST로 정의되며, 아래 map MQTT 토픽은 추후 실시간 조회·갱신용으로 별도 검토합니다.)

운영자 개입 명령(s2r/{robot_id}/action/set)은 이 예약에서 §3.1 로 옮겨 정의했습니다. 관제 화면의 정지·재가동·속도·이동 조작이 그 토픽을 씁니다.

항목토픽
maps info (실시간 조회·갱신)s2r/{robot_id}/map/get · s2r/{robot_id}/map/set · r2s/{robot_id}/map
setting infos2r/{robot_id}/setting/get · s2r/{robot_id}/setting/set · r2s/{robot_id}/setting
remote control (영상 스트림)s2r/{robot_id}/stream/get · r2s/{robot_id}/stream

10. 에러·거절 코드 (로봇 관련 발췌)

REST 인증 에러 (HTTP status)

코드HTTP의미
ROBOT_SIGNATURE_INVALID401HMAC 서명 검증 실패
ROBOT_TIMESTAMP_OUT_OF_WINDOW401timestamp ±60s 윈도우 초과 (replay 의심)
ROBOT_CERT_REVOKED401인증서 폐기됨 예약 — 현재 미사용
ROBOT_PUBLIC_KEY_INVALID422로봇 공개키(publicKey) 형식 오류 — POST /v1/robot/key
ROBOT_NOT_REGISTERED404사전 등록되지 않은 robotCode
ROBOT_ALREADY_ACTIVATED409이미 활성화된 로봇의 /key·/cert 재발급 시도 (재발급은 관제 관리자 초기화 후)

REST 맵 등록 에러 (§7)

코드HTTP의미
SITE_NOT_REGISTERED404등록되지 않은 siteId
MAP_UPLOAD_INCOMPLETE422commit 시 ① 선언 목록(files)의 파일이 일부 업로드되지 않음
MAP_VALIDATION_FAILED422commit 시 업로드 파일(이미지·yml·json) 검증 실패(파싱·필수 항목 누락 등)
MAP_UPLOAD_EXPIRED409업로드 URL 만료 후 commit 시도

MQTT 지시 거절 사유 (ack의 reason) 비스캣 제안

로봇이 s2r 지시를 수행할 수 없을 때 §3.2 ack에 result: "rejected"와 함께 싣습니다.

코드의미
mission-in-progress미션 수행 중이어서 받을 수 없는 지시(예: goto)
not-ready로봇 상태가 지시를 받을 수 없음(mode=bringup·mapping 등)
unknown-target지시가 가리키는 goal·mission·컴포넌트를 로봇이 보유하지 않음
unsupported해당 action·필드를 이 로봇이 지원하지 않음(기종·펌웨어 차이)
out-of-range인자가 허용 범위를 벗어남(limitKmh·좌표 등)
no-feasible-path목표 지점까지 경로를 만들 수 없음 — goals/bid의 불가 사유와 같은 값을 씁니다
checksum-mismatch배포 패키지 sha256 불일치(§3.3)
download-failed배포 패키지 내려받기 실패(§3.3)

거절은 실패가 아니라 답입니다 — 관제는 이 코드로 운영자에게 「왜 안 됐는지」를 그대로 보여주고, 사유별 재시도 여부를 가릅니다.

MQTT 입찰 미낙찰 사유 (goals/result의 reason)

입찰 단계의 미낙찰은 HTTP가 아니라 goals/result 메시지 goals[].reason 값으로 전달됩니다(claimed=false). 구체 사유 코드 집합은 bid 타임아웃 값과 함께 확정합니다. 사유 코드 협의

전체 에러 카탈로그·유니크 키(재시도 안전)·버전 정책은 비스캣 API 공통 규약을 따릅니다. 규약 상세는 비스캣 담당자를 통해 제공됩니다.