s2r/{robot_id}/action/set 정의(§3.1) — 정지·재가동·속도 상한·지정 이동·회피. 종전 §9 예약 항목에서 본문으로 옮겼습니다.r2s/{robot_id}/ack 정의(§3.2) — requestId echo + accepted/rejected/done. requestId를 싣는 모든 s2r 지시가 대상입니다.software/deploy · software/progress 정의(§3.3) — 설치 결과는 r2s/software 인벤토리 재발행으로 확정합니다.goals/set + status: "canceled"(§3.1 callout) — 전용 취소 토픽을 두지 않습니다.state.ems에 remote 추가(§4) — 관제 발동 정지를 로봇 자체 트리거와 구분합니다.ack의 reason 어휘.standby·doing·done(취소 canceled). 종전 ready·going·arrived·paused 표기 폐지(§1).goals/bid · quote/bid 스키마 통일 — 두 응답이 같은 goals[]{ id, arrivalOrder, available, estArrival, reason }를 씁니다. 다른 것은 질의(offer) 본문뿐입니다(§5.3 ② · §5.4).goals/bid의 available을 배차에 반영 — 종전에는 읽지 않아 "수행 불가"라고 답한 로봇도 낙찰될 수 있었습니다(§5.3 ②).quote/offer 본문 정정 — goal 전체 정의가 아니라 quoteId·deliveryType·목적지 요약의 축약형입니다(§5.4 ①).goals/set 정정 — goal 부여 채널이 아니라 부분 갱신 지시 채널입니다. goal 부여는 goals/offer 본문이 담당합니다(§5.1).box/loaded·box/unloaded 폐지 — 상차·하차 완료는 r2s/goals의 pickup:done·dropoff:done으로 일원화(§6.1).mission.type 개방형 어휘(§5), goal·mission id 발급·echo 규약(§5.1), 다중 미션 번들 발행(§5.2), 엘베 탑승·하차 보고 어휘(§5.5), nodes(json) 정규 포맷(§7).https://fms-api.zeroworks.co.kr (인증 endpoint — 환경별 분리가 필요한 경우 호스트 별도 안내)application/json; charset=utf-8. MQTT는 TLS + X.509 상호 인증 위 JSON 페이로드.{ "result": <ResultCode>, "data"?: object|string, "message"?: string }. result는 결과 코드 enum으로, SUCCESS·FAIL(일반 실패) 외 도메인별 구체 코드(ROBOT_NOT_REGISTERED·ROBOT_SIGNATURE_INVALID 등)가 사용됨. data·message는 있을 때만 노드 발현.occurredAt(ISO8601, +09:00 KST)·payload를 두고, 행위를 트리거하는 이벤트·명령(입찰·알람·PIN·박스 등)에는 멱등 처리를 위한 requestId(UUID v7)를 함께 둡니다(§3).+09:00(KST) — 모든 시각을 동일 표기 (예: 2026-06-04T14:30:00+09:00).status) 통일: state.status·goal.status·mission.status는 계층과 무관하게 같은 어휘를 씁니다 — standby(대기) · doing(수행 중) · done(완료), 취소는 canceled. 계층마다 다른 낱말(ready·going·arrived)은 쓰지 않습니다(아래 callout).X-Robot-ID, X-Robot-Ts, X-Robot-Signature (인증 endpoint 한정)상태(state)처럼 자발·고빈도로 발행되는 텔레메트리는 최신값만 의미가 있어 requestId를 생략할 수 있고, 관제는 occurredAt 비교로 더 최신일 때만 반영(last-write-wins)합니다. 반면 행위를 트리거하는 이벤트·명령(입찰 bid, 알람, PIN, 박스 닫힘 등)은 at-least-once 재전송에 대비해 requestId(또는 동등 식별자)를 유지해 멱등 처리합니다.
standby · doing · done 2026-09-03 개정상태값은 어느 계층에서든 같은 낱말을 씁니다. 계층은 무엇의 상태인가만 다르고 어휘는 공유합니다.
| 계층 | 위치 | standby | doing | done |
|---|---|---|---|---|
| 로봇 | state.status | 일감 없음 · 대기 | 일감 수행 중 | 수행 완료(다음 일감 전) |
| goal | goals[].status | 부여됐으나 아직 이동 전 | 해당 지점으로 이동 중 | 도착·해당 지점 처리 완료 |
| mission | goals[].mission[].status | 아직 시작 전 | 수행 중 | 수행 완료 |
취소는 세 계층 모두 canceled이며 사유(reason·reasonMsg)를 함께 싣습니다(로봇 발행 항목 §6).
| 폐지된 표기 | 대체 |
|---|---|
ready (goal·mission) | standby |
going (goal) | doing |
arrived (goal) | done — goal 에 별도 종료값을 두지 않습니다 |
paused (state) | 사용하지 않습니다 — 일시정지는 알람(§4.2)으로 표현 |
등록·인증은 REST로 진행합니다. 키쌍은 로봇이 직접 생성하며, 개인키는 디바이스 밖으로 내보내지 않습니다(TPM·보안 칩 권장).
POST /v1/robot/key로 제출. 관제가 robotCode 로 로봇을 조회해 내부 식별자 robotId(UUID)를 회신하며, 로봇은 이후 인증에 robotId 를 사용한다.POST /v1/robot/cert로 X.509 인증서 번들(다운로드 URL 목록) 수령 → 설치 후 MQTT 연결.로봇(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. |
/v1/robot/key — 비밀키 발급| 인증 | 없음 (사전 등록된 robotCode + 공개키) |
| 요청 | |
| 200 응답 | |
| 재발급 | 이미 발급(활성화)된 로봇이 다시 호출하면 409 ROBOT_ALREADY_ACTIVATED로 거절됩니다. 재발급이 필요하면 관제 관리자 초기화 후 다시 발급받습니다(이전 Thing·인증서는 폐기). |
| 에러 | 404 ROBOT_NOT_REGISTERED(robotCode 미등록), 422 ROBOT_PUBLIC_KEY_INVALID(공개키 형식 오류) |
/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 응답 | |
발급받은 비밀키로 요청을 서명합니다. 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
로봇(Ubuntu · C++)에서 위 규격을 구현하는 최소 예제입니다. OpenSSL 3.x 기준(빌드: g++ auth.cpp -lssl -lcrypto)이며, 지면상 에러 처리는 생략했습니다. HTTP 호출·JSON 파싱은 libcurl·nlohmann/json 등 임의 라이브러리를 사용합니다.
#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;
}
// 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 디코드 금지
}
#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;
}
인증서 설치 후 로봇은 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.1 | 1 |
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.4 | 1 |
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.1 | 1 |
r2s/{robot_id}/safety | 로봇 → 관제 | 보행자 안전 이벤트 (인지·회피·정지·재가동) — 로봇 발행 항목 §4 | 1 |
r2s/{robot_id}/software | 로봇 → 관제 | 소프트웨어 인벤토리 (컴포넌트별 버전, retain) — 로봇 발행 항목 §8 | 1 |
s2r/{robot_id}/geofence/version | 관제 → 로봇 | 단지 지오펜스 버전 알림 (retain). 본문은 REST로 조회 — §8 | 1 |
r2s/{robot_id}/geofence | 로봇 → 관제 | 영역에 의한 우회·차단 이벤트 — 로봇 발행 항목 §9 | 1 |
s2r/{robot_id}/action/set | 관제 → 로봇 | 운영자 개입 명령 (정지·재가동·속도·이동·회피) — §3.1 | 1 |
r2s/{robot_id}/ack | 로봇 → 관제 | s2r 지시 수신·판정 회신 — §3.2 | 1 |
s2r/{robot_id}/software/deploy | 관제 → 로봇 | 소프트웨어 배포 지시 — §3.3 | 1 |
r2s/{robot_id}/software/progress | 로봇 → 관제 | 배포 진행률·결과 — §3.3 | 1 |
폐지 — r2s/{robot_id}/box/loaded · r2s/{robot_id}/box/unloaded: 상차·하차 완료는 r2s/{robot_id}/goals의 pickup:done·dropoff:done으로 일원화했습니다(§6.1). 관제는 두 토픽을 구독하지 않습니다. | |||
state) 토픽은 재접속 구독자가 마지막 상태를 즉시 복구하도록 retain 사용. 입찰·알람 등 이벤트/일회성 토픽은 retain 미사용(지난 사건 재생 방지).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 은 생략
}
}
action | params | 로봇 동작 |
|---|---|---|
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 } | 현재 위치 전방의 지정 반경을 회피해 경로를 재계획합니다. 목적지는 바뀌지 않습니다 |
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로 회신합니다.
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)
}
}
| 필드 | 타입 | 설명 |
|---|---|---|
requestId | string | 대상 s2r 지시의 requestId를 변형 없이 되돌립니다 — 관제가 이 값으로 지시를 역인덱싱합니다 |
topic | string | 대상 토픽의 항목 부분(action/set·goals/set·box/open·resource/ready·software/deploy) |
result | enum | accepted(받아 수행 시작) · rejected(수행 불가) · done(수행 완료) |
reason | string | null | rejected일 때 사유 코드. 어휘는 §10 |
accepted 또는 rejected. 수행에 시간이 걸리는 명령도 이 회신은 즉시 보냅니다.done. 즉시 완료되는 명령(speed·resume)은 accepted와 done을 한 번에 done으로 보내도 됩니다.수행 결과 자체(도착·미션 진행)는 기존 r2s/{robot_id}/goals·state로 발행합니다 — ack는 지시의 종결만 다룹니다. retain 을 쓰지 않습니다(일회성 이벤트).
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 = 즉시. 값이 있으면 그 시각 이후 설치
}
}
| 필드 | 타입 | 설명 |
|---|---|---|
jobId | string | 배포 회차 식별자 — 관제가 발급하고 progress가 그대로 되돌립니다 |
components[].name | string | r2s/{robot_id}/software 인벤토리의 컴포넌트 이름과 같은 값 |
components[].version | string | 설치 목표 버전 |
components[].url | string | 패키지 내려받기 주소(만료 있는 서명 URL) |
components[].sha256 | string | 무결성 검증값. 불일치 시 설치하지 않고 failed로 보고합니다 |
scheduledAt | string | null | 예약 설치 시각(ISO-8601). null이면 즉시 |
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는 해당 컴포넌트의 이전 버전이 유지됨을 뜻합니다(부분 적용 없음).
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}/staterobot 상태가 변하는 필드값을 이벤트 단위로 로봇이 발행합니다. 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.online | boolean | - | 로봇 접속 여부 — true(온라인) / false(오프라인). 접속 시 true, 연결 끊김 시 LWT로 false. 관제는 이 값으로 활성(배차 대상) 로봇을 판별합니다(아래 접속 상태 callout). |
payload.mode | enum | - | bringup · ready · navi · cruise · delivery · trash_pickup · mapping · charging |
payload.status | enum | - | standby · doing · done (§1 status 어휘 통일 — 종전 paused는 사용하지 않습니다) |
payload.ems | string[] | - | EMS 트리거 소스 (button · door · LiDAR · bumper · remote). remote는 관제가 §3.1 action/set estop으로 발동한 정지입니다 — 로봇 자체 트리거와 구분해 운영자가 원인을 압니다 |
payload.location_score | number | 0~100 | 위치 추정 신뢰도 |
payload.location.x / y / th | number | - | 위치 좌표 및 방향 |
payload.location.map_id | string | - | 맵 식별자 (§7 맵 등록의 mapId와 동일 값) |
payload.location.lat / lng / heading | number | 도(°) | WGS84 위경도·방위각. 맵 로컬 좌표를 대체하지 않고 병기하며, 값이 없으면 세 키를 생략합니다(0 으로 채우지 않음) — 로봇 발행 항목 §2 |
payload.battery.voltage | number | V | 전압 |
payload.battery.current | number | A | 전류 |
payload.battery.soc | number | % (0~100) | 충전 상태 |
payload.battery.est_charge_complete | number | min | 충전 완료 예상 시간 |
payload.battery.est_discharge_complete | number | min | 방전 완료 예상 시간 |
payload.battery.soh | number | % (0~100) | 배터리 수명 |
payload.battery.temperature | number | °C | 배터리 온도 |
online)와 LWTonline은 로봇의 MQTT 접속 여부를 나타내며, 관제가 배차 대상(활성) 로봇을 추리는 기준입니다. 로봇은 다음과 같이 관리합니다.
online: true — MQTT 연결 직후 이벤트로 online: true를 발행하거나, 관제의 state/get 질의에 online: true로 응답합니다.online: false (LWT) — MQTT Last Will and Testament(LWT)로 등록해, 브로커가 로봇의 비정상 절단을 감지하면 대신 online: false를 발행하도록 구성합니다. 정상 종료 시에는 로봇이 종료 전 online: false를 직접 발행하는 것을 권장합니다(정상 DISCONNECT는 LWT가 발동하지 않음).state 토픽은 retain=true이므로, LWT의 online: false에도 retain(will retain)을 적용해야 이후 접속하는 관제가 마지막 접속 상태를 즉시 복구할 수 있습니다.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[].code | string | 알람 코드 |
payload.alarm[].level | enum | warn · error · fatal |
payload.alarm[].msg | string | 알람 메시지 |
s2r/{robot_id}/alarm{
"requestId": "uuid",
"occurredAt": "2026-06-04T10:00:00+09:00",
"payload": { "alarm": "reset" } // 알람 리셋 명령
}
레벨별 관제 동작 방향(예시)은 아래와 같으며, 실제 코드·레벨 매핑은 양사 시나리오 협의로 확정합니다. 정해지는 대로 관제 대응에 반영합니다.
| level | 관제 대응(예시) | 리셋 |
|---|---|---|
warn | 로그 저장 및 대시보드 표시 | 자동 |
error | 홈 복귀 및 배차 제외 → 운영자 알림 | 자동 |
fatal | 즉시 중단 및 배차 제외 → 운영자 확인 | 수동 |
로봇 서비스는 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) |
|---|---|---|---|
mode | state.mode | 로봇 서비스 종류 / 동작 모드 | bringup · ready · navi · cruise · delivery · trash_pickup · mapping · charging |
goal.type | goals[].type | 목적지(지점)의 성격 | customer(고객) · store(매장) · unloading(하역장) · charging(충전소) · waiting(대기장소) · none |
mission.type | goals[].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=delivery를 유지합니다(일감 유무·진행은 state.status·goal.status로 표현). 단 충전소 복귀는 delivery가 아니라 mode=navi + goal.type=charging(mission.type=charge)으로 전이합니다 — 관제 지시 또는 다음 배차가 없을 때 로봇이 스스로 복귀하는 timeout 자동 회차. 동작 흐름은 시나리오 §4 참조. timeout 값 협의
관제 주도 복귀와 로봇 자동 회차는 공존합니다. 관제 지시가 우선이며, 로봇은 수행할 수 없으면 goals/bid로 거절할 수 있습니다. 미션 중 처리(지금 중단할지 완료 후 이동할지)는 로봇이 정합니다.
s2r/{robot_id}/goals/offer · goals/setgoal은 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[].id | string | goal 식별자 — 관제가 발급합니다. 형식 g-{미션UUID}-{goal.type}. 로봇은 받은 값을 그대로 echo 해야 합니다(아래 callout) |
goals[].type | enum | 지점 성격 — customer · store · unloading · charging · waiting · none |
goals[].status | enum | standby · doing · done (취소 canceled) — 도착·해당 지점 완료가 done입니다. §1 status 어휘 |
goals[].arrivalOrder | number | 도착 순서. 관제는 1부터 부여하며, 로봇이 여러 미션을 통합 재계획한 뒤에는 배열 전역으로 1..N 재부여해 보고합니다(미션별로 1 부터 다시 시작하지 않음) |
goals[].location.x / y / th | number | 위치 좌표 및 방향 |
goals[].location.map_id | string | 맵 식별자 |
goals[].mission[].id | string | 미션 식별자 (= 고객 오더 단위, 여러 goal에 걸쳐 동일 id 가능). 관제 발급 형식 m-{미션UUID} |
goals[].mission[].type | string | 동작. 관제 지시값은 pickup · dropoff · wait · charge, 로봇 보고값은 개방형 — §5 서두 callout |
goals[].mission[].status | enum | standby · doing · done (취소 canceled) — §1 status 어휘 |
goals[].mission[].atomic | boolean | 미션 원자성 — 묶음 순서 보존 여부 (아래 설명) |
goals[].mission[].PIN | string | null | 적재함 인증 PIN (로봇 PIN 입력 방식 시 값, 그 외 null — §6) |
goals[].mission[].door[].name | string | 대상 도어 이름 — 관제가 배정한 적재함 칸 번호를 door{n} 형식으로 지정합니다(door1~door4). n의 상한은 로봇별 칸 수(§6.1) |
goals[].mission[].door[].status | enum | 도어 상태 — opening · opened · closing · closed (로봇이 발행) |
goals[].mission[].orderId | string | 주문 식별자 — 관제가 발급(ZWS-ORD-…/ZWS-TRA-…). 로봇은 이 값을 해석하지 않고, r2s/{robot_id}/resource 요청 시 그대로 되돌려 보냅니다(§5.5) |
goals[].mission[].address / addressDetail | string | 주소 · 상세 주소 |
goals[].mission[].phoneNumber | string | 연락처 |
goals[].mission[].message | string | 메시지 |
goals[].mission[].autoDropOff | boolean | 비대면 자동 하차 여부. 배차 로봇의 robot.auto_drop_off=true일 때만 true(하차 mode auto). 그 외 false(하차 mode attended, PIN 선전달) — §6 |
atomic은 한 미션(고객 오더)에 속한 goal들의 순서가 깨지지 않고 묶여서 유지됨을 보장하는 옵션입니다. 이후 다른 미션이 추가되어 거리 최적화로 재정렬되더라도, atomic 미션에 속한 goal들의 상대 순서는 보존되며 다른 미션의 goal이 그 사이에 끼어들지 않습니다.
goal A → goal B가 들어온 뒤, 미션 2로 goal C → goal D가 추가되는 상황atomic: false — 거리 최적화로 사이에 끼어들 수 있음 → 예: A, C, B, Datomic: true — A·B 묶음이 보존됨(C·D가 사이에 끼지 못함) → 예: A, B, C, Dg-{미션UUID}-{goal.type} (예: g-01a0324f-eb55-7ba6-a670-91ace2d98ca5-store) · mission: m-{미션UUID}.r2s/{robot_id}/goals·goals/bid·quote/bid에서 받은 id 를 변형 없이 사용해야 합니다. 관제는 goals/bid의 goal id 로 입찰 라운드를 역인덱싱하고, r2s goals 배열에서는 goal id 접두만으로 미션을 가릅니다(goal 레벨에 orderId가 없습니다).charging·waiting·none)은 이 접두를 쓰지 않으며, 관제는 정상 트래픽으로 보고 무시합니다.goal.type=charging · mission[].type=charge 를 goals/offer 로 보냅니다. 전용 토픽은 없습니다.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" } ] }
]
}
}
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[].estArrival | string | ISO-8601 | 도착 예상 시각 — occurredAt과 동일 포맷(예 2026-08-21T14:03:00+09:00) |
goals[].progress | number | 0~100 | 진행률 |
goals[].status | enum | - | 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분 전 고객 알림도 이 환산값으로 판정합니다.
null로 되돌리지 않고 마지막 유효값을 유지합니다. null은 알림 판정에서 제외됩니다.occurredAt·estArrival을 찍는 로봇 시계는 NTP 로 동기를 유지해야 합니다(실측상 1분 이상 오차 사례 있음).입찰은 3단계입니다. 관제가 후보 로봇에 offer로 제시 → 각 로봇이 bid로 goal별 도착 예상 시각 응답 → 관제가 전체 ETA(마지막 goal 도착 예상 시각 기준 남은 시간) 최소 로봇으로 배차를 결정(추후 적재공간 기준 포함)해 result로 낙찰 여부를 통보합니다. 어느 로봇이 수행할지의 최종 배차 결정은 관제가 단독으로 가집니다(배차 확정·충돌 방지).
입찰·낙찰은 goalId 단위로 식별합니다(별도 오퍼 식별자 없음). 라운드는 단발이며 재오퍼하지 않습니다 — bid 타임아웃 시점에 관제가 모인 입찰로 배차를 결정하고, 마감 후 도착한 지각 bid는 배제합니다. (로봇, goal)당 in-flight 오퍼는 1건입니다. 입찰 가능한 로봇이 없으면 해당 미션은 수행 불가로 종료됩니다. 타임아웃 값 협의
online: true인 로봇 전체에 offer를 보냅니다(활성 판별은 §4.1 접속 상태 callout 참조). 오프라인 로봇은 offer 대상에서 제외됩니다.online 필터는 사전 선별일 뿐이므로, 응답 없는 로봇을 걸러내는 최종 기준은 이 타임아웃입니다. 운용 중 조정s2r/{robot_id}/goals/offergoal 전체 정의(§5.1 본문)를 그대로 실어 보냅니다 — id·type·status·arrivalOrder·location과 mission[](PIN·door·orderId·addressDetail·autoDropOff 포함). 낙찰 로봇은 이 본문을 그대로 수행하며, 별도의 goal 재전송은 없습니다.
r2s/{robot_id}/goals/bidquote/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 반영false면 그 로봇은 채택 대상에서 빠집니다 — ETA 가 가장 빨라도 낙찰되지 않습니다. 입찰한 로봇 전원이 불가면 배차는 즉시 실패하며, 파트너에게 "입찰 로봇 전원이 수행 불가로 응답"으로 회신됩니다.available=false일 때 estArrival이 null이어도 정상 응답으로 처리합니다.available을 싣지 않으면 가용으로 읽습니다 — 필드를 아직 보내지 않는 펌웨어의 하위호환입니다.s2r/{robot_id}/goals/resultoffer 로 제시한 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[].id | string | offer 의 goal id — 관제 발급값 그대로 bid · result |
goals[].arrivalOrder | number | 도착 순서 bid |
goals[].available | boolean | 수행 가능 여부 bid |
goals[].estArrival | string | null | 도착 예상 시각(ISO-8601, occurredAt과 동일 포맷 — §5.2 규약 참조). 불가 시 null bid |
goals[].reason | string | null | bid: 불가 사유(available=false 시) · result: 미낙찰 사유(현재 outbid) bid · result |
goals[].claimed | boolean | 낙찰 여부 result |
주문이 확정되기 전, 관제가 후보 로봇에게 "지금 이 배송을 수행할 수 있는지"를 미리 물어 가용성·예상 도착시간을 받아 두는 사전 견적입니다. 미션(goal)을 생성하지 않는 비구속 질의이므로, 배차 입찰(§5.3 goals/*)과 토픽을 분리해 quote/* 네임스페이스로 운영합니다. 낙찰(result) 단계가 없어 입찰의 3단계를 2단계(질의 → 견적)로 축약하며, 응답(bid) 스키마는 배차 입찰과 완전히 동일합니다. 관제는 모든 goal 이 available인 로봇 중 전체 ETA(마지막 goal 도착 예상 시각 기준 남은 시간) 최소를 가용 ETA로 집계합니다(배차 낙찰과 동일 방식).
배차 입찰 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 }
]
}
}
| 필드 | 타입 | 설명 |
|---|---|---|
quoteId | string | 견적 라운드 식별자(UUID v7). 응답에는 싣지 않습니다 — 상관은 토픽 robot_id로 합니다 |
deliveryType | enum | delivery(배송) · trash_pickup(수거). 목적지 순서가 달라집니다 — 배송 매장→세대, 수거 세대→수거장 |
goals[].type | enum | 목적지 성격 — goal.type과 같은 어휘(store · customer · unloading) |
goals[].location | object | 목적지 좌표(§5.1과 동일 구조). 노드 좌표를 못 찾으면 생략됩니다 |
goals[].boxCount | number | 필요한 적재함 칸 수. 상차가 일어나는 세대(customer) goal 에만 싣습니다 |
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[].id | string | - | 견적에는 goal id 가 없으므로 빈 문자열로 둡니다. 상관은 토픽 robot_id로 합니다 |
goals[].arrivalOrder | number | - | 질의의 goals[] 순서에 대응하는 도착 순서 |
goals[].available | boolean | - | 수행 가능 여부 |
goals[].estArrival | string | null | ISO-8601 | 도착 예상 시각(occurredAt과 동일 포맷 — §5.2 규약 참조, 불가 시 null) |
goals[].reason | string | null | - | 불가 사유 (available=false 시) |
입찰과 동일하게 단발 라운드(재질의 없음)이며, bid 타임아웃도 배차와 동일(초기값 3초)입니다. 타임아웃 내 무응답 로봇은 available=false로 간주하고 지각 응답은 배제합니다. 후보 전원이 unavailable·무응답이면 가용성 결과는 수행 가능 로봇 없음으로 집계됩니다. 상관(어느 라운드 응답인지)은 토픽의 robot_id로 판별합니다 — 견적 응답에는 별도 견적 식별자가 없습니다. 운용 중 조정
로봇이 미션 수행 중 엘리베이터·로비폰(공동현관)이 필요한 시점에 관제로 요청합니다. 주소 정보는 관제가 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 네임스페이스가 갈리는 사례가 관측됨).
엘베는 호출 후 탑승·하차 단계가 진행됩니다. 관제는 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" — 탑승 실패로 로봇이 콜을 포기할 때
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" }
고객 수령(하차) 방식은 로봇 H/W의 비대면 지원 여부(robot.auto_drop_off)로 결정되며, 배차/입찰 시점에 goal.mission으로 전달됩니다. 비스캣은 대면 배송에서 항상 userPin(4자리)을 발행하고 오토메타 배차 응답에 동봉합니다.
| 하차 mode | PIN | autoDropOff | 동작 |
|---|---|---|---|
비대면 (auto) — 로봇 auto_drop_off=true H/W 한정 | null | true | 도착 즉시 박스 자동 개방·하차 후 복귀 (고객 조작 불필요) |
대면 (attended) — 기본 | 랜덤(4자리) | false | ① 로봇 키패드 PIN 입력(로컬 검증) 또는 ② 오토메타 웹 개방(관제→로봇 문열림 지시) |
mission.PIN으로 선전달하고 로봇이 로컬 검증합니다. 검증 후 사용·결과(성공/실패·박스 개폐 인가)를 r2s 감사 이벤트로 보고합니다(보고 토픽·페이로드 형식 확정 대상).attended) 배송마다 랜덤 4자리를 생성·오토메타 배차 응답(userPin 필드)에 동봉합니다.autoDropOff 결정 주체: 배차 시 관제가 담당 로봇의 robot.auto_drop_off를 보고 결정합니다.s2r/{robot_id}/box/openbox/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 |
개방 지시는 관제가 박스 번호(boxSlots)를 지정해 내려보냅니다. 로봇은 주문번호를 해석하지 않습니다 — 관제가 주문번호↔박스 번호 매핑을 관장하고, 로봇은 지정된 물리 칸만 엽니다. 이 메시지는 행위를 트리거하는 명령이므로 §1 규약대로 requestId(멱등)를 유지하고 QoS 1로 전송합니다. 개방 지시는 로봇이 해당 지점에 도착한 뒤(점포=상차, 세대=하차)에만 유효합니다.
s2r/{robot_id}/box/open으로 지정 박스(boxSlots)를 엽니다(PIN 입력 불필요). 본 절이 그 문열림 지시의 구체 계약입니다.mission.PIN을 로컬 검증 후 스스로 개방하는 경로로, box/open 지시 없이 진행됩니다. 이때의 PIN 감사 이벤트(r2s) 형식은 별도 확정 대상입니다.autoDropOff=true): 도착 즉시 로봇이 자동 개방하며 box/open 지시·PIN 없이 진행됩니다(고객 조작 불필요).관제가 열 박스 번호(boxSlots)를 지정해 지시합니다. 관제가 주문번호에 매핑한 번호를 내려주며, 로봇은 지정된 물리 박스 칸만 개방하고 주문번호는 해석하지 않습니다. 상차(점포)·하차(세대) 공용입니다.
{
"requestId": "uuid",
"occurredAt": "2026-07-19T14:00:00+09:00",
"payload": {
"boxSlots": [1, 3], // 관제가 지정한 개방 박스 번호
"orderId": "ZWS-ORD-..." // 선택 — 상관용. 로봇은 해석하지 않습니다
}
}
| 필드 | 타입 | 설명 |
|---|---|---|
boxSlots | number[] | 개방할 박스 번호 배열. 관제가 주문번호에 매핑해 지정 — 로봇은 해당 물리 칸만 개방. 번호 범위는 1..N이며 N은 로봇별 칸 수(관제에 로봇마다 등록, 하드웨어 상한 4) |
orderId | string | null | 상관용 불투명 토큰(선택). 배차 경로의 개방 지시에서만 채워지며, 로봇은 해석하지 않고 무시해도 됩니다 |
별도 토픽 없이 r2s/{robot_id}/goals(§5.2)로 보고합니다.
pickup/dropoff mission 이 status: "doing". 관제는 이 보고를 개방 완료(오토메타 unlock 응답)로 처리합니다.status: "done". 매장이면 배송 출발, 세대면 수령 완료·미션 종료로 처리합니다.boxSlots 2개 이상) 시 일부만 열린 경우의 표현.pickup:done·dropoff:done 미도달 시 관제 처리).로봇이 보유한 지도와 의미 거점 노드(관제가 goal을 내릴 때 참조하는 명명된 지점)를 siteId 기준으로 관제에 등록합니다. 로봇 UI의 업로드 버튼을 누르면 로봇이 보유 맵을 자동 업로드합니다. 흐름은 등록 개시 → 파일 업로드 → 등록 확정 3단계로, 등록 개시 시 siteId·제시 mapId와 업로드할 파일 목록을 선언하면 관제가 파일별 업로드 URL을 발급하고, 로봇은 지도 이미지·메타(yml)·노드(json) 파일을 각각 해당 URL로 전송합니다(압축하지 않음).
mapId는 로봇(제로웍스)이 제시하며, MQTT 상태·goal의 location.map_id와 동일 값입니다. (siteId, mapId) 조합당 mapVersion이 증가하며 이력을 보관합니다. 등록 확정 시 최신본이 활성 맵이 되고 이전 버전은 비활성으로 전환됩니다(롤백·감사 가능). 관제는 등록된 노드 좌표를 goal의 location으로 사용합니다.
/v1/robot/maps인증: HMAC(로봇). siteId·제시 mapId와 함께 업로드할 파일 목록(files)을 선언하면, 관제가 mapVersion과 선언한 파일별 업로드 URL을 발급합니다. 지도 메타·노드는 본문에 싣지 않고 ②에서 각 파일로 업로드합니다.
| 인증 | HMAC (로봇) |
| 요청 | |
| 200 응답 | |
| 에러 | 404 SITE_NOT_REGISTERED |
| 필드 | 타입 | 설명 |
|---|---|---|
siteId | string | 단지·현장 식별자 (맵 매핑 기준) |
mapId | string | 로봇(제로웍스)이 제시하는 맵 식별자. MQTT location.map_id와 동일 값을 사용 |
files[].name | string | 업로드할 파일명 |
files[].type | enum | image(지도 이미지) · meta(yml) · nodes(json) |
{uploadUrl}로봇이 ① 응답의 uploads[]에 따라 각 파일을 해당 업로드 URL에 그대로 PUT합니다(압축하지 않음). uploadExpiresIn 내에 완료해야 합니다.
파일(type) | 내용 |
|---|---|
| image | occupancy grid 등 지도 이미지 (pgm/png 등) |
| meta (yml) | 지도 메타데이터 — 해상도(m/px)·원점(x/y/th)·크기 등 |
| nodes (json) | 주행 그래프 노드 목록. 그중 Place 객체를 가진 노드가 목적지(관제가 goal 좌표로 사용) — 아래 포맷 참조 |
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 목록(관제 미사용, 로봇 주행 그래프) |
/v1/robot/maps/{mapId}/commit업로드 완료를 통보합니다. 관제가 ① 선언 목록의 모든 파일이 업로드됐는지 확인하고, 각 파일을 검증(파싱·필수 항목 확인)해 통과하면 활성화(이전 버전 비활성)합니다. 선언 파일이 하나라도 누락됐거나 검증에 실패하면 거절하며, 선언에 없는 파일은 반영하지 않고 삭제합니다.
| 인증 | HMAC (로봇) |
| 200 응답 | |
| 에러 | 422 MAP_UPLOAD_INCOMPLETE(선언 파일 누락), 422 MAP_VALIDATION_FAILED(파일 검증 실패), 409 MAP_UPLOAD_EXPIRED(업로드 URL 만료 후 commit) |
관제 콘솔에서 정의한 다각형 영역을 로봇이 내려받아 보유하고, 경로 계획·주행 중 스스로 판정합니다. 버전 알림은 MQTT, 본문 조회는 REST입니다. 동작 흐름은 시나리오 §9, 로봇이 발행하는 항목은 로봇 발행 항목 §9를 따릅니다.
버전은 단지(siteId) 단위로 매겨지며 영역 건별 버전은 없습니다. 영역의 등록·수정·삭제·활성 변경 시마다 증가합니다. 로봇은 보유 버전과 일치하지 않을 때 내려받습니다 — 수신 버전이 보유 버전보다 작은 경우(되돌림 배포)에도 내려받아야 합니다.
s2r/{robot_id}/geofence/version단지 지오펜스 버전이 바뀌면 그 단지의 모든 로봇 토픽에 발행합니다. retain이므로 재접속한 로봇은 마지막 버전을 자동으로 수신합니다.
| QoS · retain | 1 · true |
| 본문 | §1 의 공통 envelope 을 따릅니다. 발행 시각은 occurredAt 입니다. |
/v1/robot/geofences인증: HMAC(로봇) — 맵 등록(§7)과 동일 방식입니다. 로봇이 소속된 단지의 활성 영역 전체를 반환합니다.
| 인증 | HMAC (로봇) |
| 요청 | 본문 없음. 대상 단지는 로봇 식별자로 결정됩니다. |
| 200 응답 | |
| 에러 | 401 UNAUTHORIZED(서명 검증 실패), 404 ROBOT_NOT_REGISTERED(미등록 로봇), 404 SITE_NOT_REGISTERED(로봇에 단지 미배정) |
version이 ①에서 수신한 값과 다르면 그 사이 다시 바뀐 것입니다. 응답 값을 적용 버전으로 삼고 다음 알림을 기다립니다.effectiveFrom·effectiveTo는 로봇이 자체 시계로 매 판정 시점에 대조합니다. 기간의 시작·종료로는 버전이 바뀌지 않습니다.적용 버전 회신(state.geofence)과 우회·차단 이벤트(r2s/{robot_id}/geofence)의 필드 규격은 로봇 발행 항목 §9에 있습니다.
아래 항목은 1.5차 PoC 범위 밖이며 토픽 자리만 예약합니다. (지도 파일 등록은 §7 REST로 정의되며, 아래 map MQTT 토픽은 추후 실시간 조회·갱신용으로 별도 검토합니다.)
| 항목 | 토픽 |
|---|---|
| maps info (실시간 조회·갱신) | s2r/{robot_id}/map/get · s2r/{robot_id}/map/set · r2s/{robot_id}/map |
| setting info | s2r/{robot_id}/setting/get · s2r/{robot_id}/setting/set · r2s/{robot_id}/setting |
| remote control (영상 스트림) | s2r/{robot_id}/stream/get · r2s/{robot_id}/stream |
| 코드 | HTTP | 의미 |
|---|---|---|
ROBOT_SIGNATURE_INVALID | 401 | HMAC 서명 검증 실패 |
ROBOT_TIMESTAMP_OUT_OF_WINDOW | 401 | timestamp ±60s 윈도우 초과 (replay 의심) |
ROBOT_CERT_REVOKED | 401 | 인증서 폐기됨 예약 — 현재 미사용 |
ROBOT_PUBLIC_KEY_INVALID | 422 | 로봇 공개키(publicKey) 형식 오류 — POST /v1/robot/key |
ROBOT_NOT_REGISTERED | 404 | 사전 등록되지 않은 robotCode |
ROBOT_ALREADY_ACTIVATED | 409 | 이미 활성화된 로봇의 /key·/cert 재발급 시도 (재발급은 관제 관리자 초기화 후) |
| 코드 | HTTP | 의미 |
|---|---|---|
SITE_NOT_REGISTERED | 404 | 등록되지 않은 siteId |
MAP_UPLOAD_INCOMPLETE | 422 | commit 시 ① 선언 목록(files)의 파일이 일부 업로드되지 않음 |
MAP_VALIDATION_FAILED | 422 | commit 시 업로드 파일(이미지·yml·json) 검증 실패(파싱·필수 항목 누락 등) |
MAP_UPLOAD_EXPIRED | 409 | 업로드 URL 만료 후 commit 시도 |
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) |
goals/result의 reason)입찰 단계의 미낙찰은 HTTP가 아니라 goals/result 메시지 goals[].reason 값으로 전달됩니다(claimed=false). 구체 사유 코드 집합은 bid 타임아웃 값과 함께 확정합니다. 사유 코드 협의