
물리적 API (Physical API) 구축하기
요약
로봇의 물리적 상태를 데이터화하여 관측 가능성(Observability)을 확보하는 '물리적 API' 구축 방법을 다룹니다. CAN 버스를 통해 모터의 위치, 속도, 온도 등의 데이터를 실시간으로 수집하고 Rust 기반의 sentinel 프로그램을 통해 효율적으로 전송하는 구조를 설명합니다.
핵심 포인트
- 물리적 API: 로봇의 동작을 텔레메트리, 이력, 피드백 데이터로 노출하는 계층
- 관측 가능성 확보: 모터 과열이나 전압 강하 등 하드웨어 문제를 사전에 감지
- CAN 버스 활용: 브로드캐스트 매체를 이용해 제어 루프에 영향을 주지 않고 데이터 수집
- Rust 기반 Sentinel: Raspberry Pi에서 실행되는 경량 프로세스로 데이터 디코딩 및 배치 전송
첫 번째 기사에서 저는 부품을 출력하고 모터를 움직이는 것부터 PS4 컨트롤러를 이용한 원격 조작 (teleoperating), 시연 데이터 수집, 그리고 테이블 위의 병을 집어 올리는 모델을 학습시키는 과정까지 로봇을 구축하는 이야기를 들려드렸습니다. 마지막에 저는 제가 '물리적 API (physical API)'라고 불러온 아이디어를 언급했습니다. 이는 로봇이 물리적 세계에서 수행하는 작업을 데이터, 즉 원격 측정 (telemetry), 이력 (history), 그리고 향후 학습을 안내하는 피드백으로 노출하는 계층을 의미합니다. 이 기사는 그 계층의 첫 번째 큰 조각인 관측 가능성 (observability)에 관한 것입니다.
모델이나 원격 조작 (teleop)이 실행되는 동안, 하드웨어에 대한 저의 관점은 터미널에 출력되는 내용이 전부였습니다. 빠른 테스트를 위해서는 그것으로 충분하지만, 제가 피하고 싶은 실패들은 그곳에 나타나지 않습니다. 예를 들어 20분간의 에피소드 동안 모터가 서서히 과열되거나, 6개의 관절이 동시에 힘을 쓸 때 전원 공급 장치의 전압이 떨어지거나, 제어 루프 (control loop)가 전체 설계가 가정하는 속도보다 조용히 낮게 실행되는 경우 등입니다. 관측 가능성 (observability)은 모터가 타버리거나 잘못된 데이터 세션이 끝난 후에 이를 발견하는 대신, 이러한 일들이 다가오고 있음을 미리 볼 수 있게 해주는 방법이며, 또한 무엇이 정상인지 학습하여 비정상적인 상태가 눈에 띄게 만드는 방법이기도 합니다. 큰 노력 없이 이를 구축할 수 있었던 이유는 원재료가 이미 사용 가능했기 때문입니다. 그 모든 답변은 항상 CAN 버스 (CAN bus)를 통해 전달되고 있었고, 저는 그저 듣기만 하면 되었습니다.
버스 (Bus)에 귀 기울이기
MIT 모드에서는 모터가 받는 모든 명령이 응답을 생성합니다. 각 응답 프레임 (reply frame)은 모터의 위치 (position), 속도 (velocity), 토크 (torque), 그리고 온도 (temperature)를 8바이트에 담아 전달합니다. 초당 100번 명령을 받는 6개의 모터가 있다면, 이는 초당 약 600프레임의 실시간 하드웨어 상태가 선로 (wire)를 통해 전달되어 드라이버가 모터당 유지하는 메모리 박스에 입력된 후 폐기됨을 의미합니다.
CAN은 브로드캐스트 매체 (broadcast medium)입니다. 버스 상의 모든 노드는 모든 프레임을 볼 수 있으며, 이를 읽는 것은 다른 참여자들에게 아무런 비용을 발생시키지 않습니다. 따라서 텔레메트리 (telemetry) 문제는 동일한 버스를 열고, 지나가는 프레임을 디코딩하여, 한 번도 전송하지 않은 채 유용한 곳으로 보내는 프로그램을 만드는 문제로 축소됩니다. 제어 루프 (control loop)는 이 프로그램의 존재를 알지 못합니다.
저는 이 프로그램을 Rust로 작성하고 sentinel이라고 불렀습니다. 이 프로그램은 로봇의 머리에 있는 Raspberry Pi에서 실행되며, 원격 조종 (teleop) 옆에서 별도의 프로세스로 동작하며 구조는 작습니다. 하나의 스레드는 CAN 소켓을 읽고 상태 프레임을 디코딩하며, 두 번째 스레드는 초당 한 번씩 깨어나 축적된 데이터를 비우고(drain) HTTP를 통해 제 API로 배치 (batch)를 POST 합니다. 이 두 스레드 사이에는 유한 채널 (bounded channel)이 위치합니다. 리더 (reader) 또한 들리는 모든 것을 유지하지 않습니다. 각 모터는 초당 100번 자신의 상태를 보고하는데, 이는 모니터링에 필요한 것보다 훨씬 많으므로 초당 10개의 샘플만 통과시키고 나머지는 폐기합니다. 이를 통해 저장 비용을 10배 저렴하게 만들 수 있습니다.
제가 가장 중요하게 생각하는 설계 결정은 텔레메트리가 최선 노력 (best effort) 방식이라는 점입니다. 만약 API가 느려지거나 오프라인 상태가 되면, 채널이 가득 차게 되고 리더는 샘플을 큐잉 (queueing)하는 대신 버리기 시작하며, 대신 손실된 샘플의 개수만 유지합니다. 이 개수는 성공적인 다음 배치에 포함되어 전달되므로, 데이터의 공백이 스스로를 설명하게 됩니다. 저는 세션 중에 40분 동안 API를 다운시켜 이 방식을 테스트했습니다. Sentinel은 안정적인 메모리 상태를 유지하며 계속 실행되었고, API가 복구되었을 때 첫 번째 배치는 약 14만 개의 폐기된 샘플을 보고했습니다. 이는 6개의 모터가 초당 10개의 샘플을 40분 동안 축적했을 때의 양과 일치하므로, 손실 계산이 정확함을 확인할 수 있었습니다. 자신이 감시해야 할 기계를 다운시킬 때까지 큐를 키워나가는 텔레메트리 에이전트라면, 차라리 에이전트가 없는 것보다 더 나쁠 것입니다.
응답을 듣는다는 것은 명령(commands)을 듣는다는 것을 의미하기도 합니다. 명령 프레임(command frames)은 각 관절(joint)의 목표 위치(target position)를 담고 있으므로, sentinel은 마지막으로 확인한 목표 위치를 기억하고 모든 샘플에 이를 부착합니다. 관절이 있어야 할 곳과 실제 위치 사이의 차이는 추적 오차 신호(tracking error signal)가 되며, 이는 가장 유익한 차트 중 하나로 밝혀졌습니다. 작은 일정한 오프셋(offset)은 유한한 이득(gain)에 대항하는 중력의 작용이며, 일시적인 스파이크(transient spike)는 빠른 움직임을 의미하고, 지속적인 오차는 관절이 장애물과 싸우고 있음을 나타냅니다.
로봇이 조용할 때
전원은 켜져 있지만 분리된(disengaged) 상태의 로봇은 상태 프레임(status frames)을 보내지 않으며, 바로 그때가 제가 전원 공급 장치를 관찰하고 싶은 시점입니다. 이러한 경우를 위해 sentinel에는 액티브 모드(active mode)가 있습니다. sentinel은 버스(bus)가 침묵 상태가 된 것을 감지하고, 모터들에게 초당 한 번씩 버스 전압(bus voltage)과 각 모터의 전류(current)를 묻기 시작하며, 모든 모터가 차례대로 응답할 수 있도록 순환하며 질문합니다. 응답은 동일한 버스를 통해 돌아오며, 동일한 디코딩 경로(decoding path)가 이를 포착합니다.
버스에 데이터를 쓰는 것이 수동적(passive) 설계와 모순되는 것처럼 들릴 수 있지만, CAN 자체에서 이 문제가 해결됩니다. 두 노드가 동시에 전송할 때, 수치적으로 더 낮은 식별자(identifier)를 가진 프레임이 중재(arbitration)에서 승리합니다. 파라미터 읽기(parameter read) 프레임은 모든 제어 및 상태 프레임보다 더 높은 식별자를 가지므로, 저의 질문은 항상 제어 트래픽(control traffic)에 밀리게 됩니다. 로봇의 작업으로 인해 질문이 지연될 수는 있지만, 질문이 로봇을 지연시킬 수는 없습니다. 이러한 특성 덕분에 나중에 원격 조작(teleoperation) 중에도 전압 읽기(voltage readings)를 계속 유지할 수 있었습니다.
API와 데이터베이스
수신 측은 의도적으로 지루할 만큼 단순합니다. 이는 TimescaleDB를 백엔드로 사용하는 FastAPI 서비스입니다. TimescaleDB는 시계열(time series)을 위한 Postgres 확장 기능으로, 큰 테이블을 시간 순서대로 정렬된 청크(chunks)로 분할하므로, 아무리 많은 과거 데이터가 쌓여 있더라도 최근 윈도우(window)에 대한 쿼리는 빠르게 유지됩니다. sentinel은 배치(batches)를 POST로 전송하고, API는 행(rows)을 기록하며, 보존 정책(retention policy)은 30일이 지난 모든 데이터를 삭제합니다.
API의 표면(surface)은 작습니다. 텔레메트리 (Telemetry) 데이터는 두 개의 엔드포인트 (endpoint)를 통해 들어오는데, 하나는 관절 샘플 (joint samples)용이고 다른 하나는 전압 측정값 (voltage readings)용이며, 각각 헤더 (header)의 키 (key)와 함께 요청당 하나의 배치 (batch)를 받습니다. 로봇을 어디에도 별도로 등록할 필요는 없습니다. 새로운 로봇 ID와 함께 도착하는 첫 번째 배치가 해당 로봇을 생성하며, 그 이후의 모든 배치는 마지막 확인 타임스탬프 (timestamp)를 갱신합니다. 이는 하위 시스템 (downstream)의 모든 요소가 로봇의 온라인 여부를 결정하는 방식이기도 합니다.

읽기 측에는 세 개의 엔드포인트가 있습니다. 하나는 로봇 목록을 나열하고, 하나는 로봇의 현재 스냅샷 (snapshot)을 반환하며, 하나는 차트용 메트릭 (metric)의 이력을 반환합니다. 스냅샷은 들리는 것보다 더 많은 작업을 수행하는데, 데이터 조각들이 서로 다른 리듬으로 도착하기 때문입니다. 각 관절의 가장 최신 상태 샘플, 마지막 전압 측정값, 마지막 전류 측정값, 그리고 수집기 (collector) 자체의 상태 보고서 (health report)가 모두 서로 다른 타임스탬프를 가진 서로 다른 행 (rows)에 존재하므로, 엔드포인트는 각 항목의 최신 데이터를 수집하여 모든 것을 하나의 응답으로 병합합니다. 이력 (history) 엔드포인트는 데이터베이스 (database) 측에서 집계 (aggregate)를 수행하여 원시 샘플 (raw samples)을 시간 버킷 (time buckets)으로 그룹화합니다. 따라서 15분 단위의 창 (window)이든 하루 전체든, 원시 행의 덤프 (dump) 대신 대략 200개의 포인트 (points)로 반환됩니다.
수집기가 데이터베이스에 직접 쓰도록 두는 대신 HTTP API를 거치게 하는 부분이 바로 이것을 단순한 스크립트 (script)가 아닌 레이어 (layer)로 만드는 요소입니다. 동일한 키를 가진 동일한 문이 제가 다음에 추가할 로봇에게도 적용되며, 이 문은 첫 번째 글에서 언급한 인간 피드백 (human feedback)이 들어오는 문이기도 합니다. 각 판결 (verdict)은 해당 판결 대상이 된 에피소드 (episode)의 텔레메트리 옆에 기록되어 다음 데이터셋 (dataset)에 무엇을 포함할지 결정하는 데 도움을 줍니다. 이 흐름은 별도의 글을 다룰 가치가 있으므로, 자세한 내용은 다음 포스트로 남겨두겠습니다.
시작점으로서의 Grafana
나만의 인터페이스를 작성하기 전에, 먼저 Grafana를 데이터베이스에 연결했습니다. 대시보드는 저장소(repo) 내에서 코드로서 프로비저닝(provisioned as code)되므로, 스택을 올리면 대시보드들이 재생성됩니다. 덕분에 하루 만에 버스 전압(bus voltage)을 큰 숫자로 표시하고, 각 모터의 온도와 전류를 원형 게이지(circular gauges)로 나타내며, 시간에 따른 추적 오차(tracking error)를 확인하고, 실제로 우려되는 상황들—모터 온도가 50도를 넘거나, 전압이 45V 미만으로 떨어지거나, 로봇이 너무 오랫동안 침묵하는 경우—에 대한 경고 규칙(alert rules)을 설정할 수 있었습니다.
각 패널은 서로 다른 종류의 질문에 답합니다. 모터별 온도와 전류는 하드웨어가 고통받고 있는지를 알려줍니다. 열은 긴 세션 동안 천천히 쌓이기 때문에 게이지가 급격히 변하는 대신 서서히 움직이며, 각 관절의 전류는 모터가 얼마나 힘들게 작동하는지를 보여줍니다. 어깨 관절이 대부분의 부하를 견디고 있으며, 스파이크(spikes)는 팔이 저항에 부딪히는 순간을 표시합니다. 전압 패널은 동일한 부하에 대해 전원 공급 장치가 어떻게 반응하는지 감시합니다. 추적 오차(Tracking error)는 움직임 자체를 다루며, 관절별 위치, 속도, 토크 차트를 통해 세션을 되감아 보며 무언가 이상해 보이는 순간에 팔이 무엇을 하고 있었는지 확인할 수 있습니다. 제어 루프 속도(control loop rate)는 소프트웨어를 감시합니다. 그리고 두 개의 작은 패널은 감시자를 감시하는데, sentinel이 폐기해야 했던 샘플의 개수와 가장 최신 데이터의 연령(age)을 보여줌으로써 대시보드의 나머지 정보를 언제 불신해야 하는지 알 수 있게 해줍니다. 관절 필터(joint filter)를 사용하면 특정 대상을 추적할 때 모든 데이터를 단일 모터로 좁힐 수 있습니다.
실제 로봇을 가동하자 숫자들에 질감이 생겼습니다. 전원 공급 장치는 유휴 상태에서 47.8V를 유지하다가 부하가 걸리면 눈에 띄게 떨어집니다. 어깨 피치(shoulder pitch) 모터는 중력에 저항하는 것 외에는 아무것도 하지 않을 때 약 5Nm(newton meters)를 유지합니다. 제어 루프는 상태 프레임(status frames)을 카운트하여 측정했을 때 99에서 101Hz 사이를 보고하며, 이는 어떤 차단 호출(blocking call)이 조용히 루프 속도를 떨어뜨리는 종류의 버그를 잡아낼 수 있습니다.
Grafana는 내가 로봇을 저렴한 비용으로 관찰할 수 있는지에 대한 질문에 답을 주었으며, 맞춤형 구축에 투자하기 전에 어떤 시각화가 중요한지를 가르쳐 주었습니다. 이는 여전히 나의 엔지니어링 콘솔(engineering console)로 남아 있습니다.
제어 평면 (The control plane)
엔지니어링 콘솔 (engineering console)은 그것을 만든 사람에게는 유용하게 작동합니다. 하지만 그 외의 사람들에게 차트가 가득한 벽은 너무 많은 것을 요구합니다. 어떤 패널이 중요한지, 어떤 임계값 (threshold)이 정상인지, 어떤 숫자를 무시해도 안전한지를 알아야 하기 때문입니다. 제어 평면 (control plane)은 그에 대한 저의 해답입니다. 이는 프로젝트의 시각적 언어로 구현된 웹 프론트엔드 (web frontend)로, 동일한 API로부터 데이터를 읽어오며, 첫눈에 중요한 질문들에 대한 답을 제공합니다. 로봇이 온라인 상태인지, 전력을 얼마나 소모하고 있는지, 과열된 부분이 있는지와 같은 질문들 말입니다.
메인 페이지에는 로봇 한 대당 카드 하나씩, 전체 플릿 (fleet)이 표시됩니다. 각 카드에는 로봇의 상태, 버스 전압 (bus voltage), 총 전류 소모량, 그리고 각 모터의 온도와 전류가 포함된 모터 테이블이 담겨 있습니다. 이는 Grafana 대시보드에서 가져온 것과 동일한 신호들이지만, 로봇 옆에 서 있는 사람이 실제로 알고 싶어 하는 정보로 축소되었으며, 로봇이 온라인 상태일 때 생동감 있게 움직이는 작은 손그림 아이콘과 같은 개성을 담아 제시됩니다. 사람들이 즐겁게 바라보는 페이지는 더 자주 보게 되며, 모니터링에 있어서 그것은 업무의 절반을 차지합니다.
로봇을 클릭하면 해당 로봇의 페이지가 열립니다. 중심부에는 버스 (bus)에서 들어오는 관절 위치에 따라 움직이는 암 (arm)의 실시간 3D 모델이 있습니다. 따라서 실제 암을 원격 제어 (teleoperating)하면, 앞서 설명한 수동적 리스닝 (passive listening)만으로 페이지 위의 복제본이 잠시 후 움직이게 됩니다. 그 주변으로 페이지는 수집기 (collector) 자체의 상태, 루프 속도 (loop rate), 데이터 신선도 (staleness) 및 폐기된 샘플 (discarded samples)과 함께, 최근 구간 동안의 전압 및 추적 오차 (tracking error) 차트를 보여줍니다.
이 페이지의 모터 (motors) 테이블은 이전에는 답할 방법이 없었던 질문, 즉 각 모터가 정확히 어떤 펌웨어 (firmware)를 실행하고 있는지에 대한 답도 제공합니다. 모든 관절 (joint)은 실시간 온도 및 전류 (current) 옆에 모터 모델과 펌웨어 버전을 표시하므로, 이 페이지는 단순한 모니터가 아닌 로봇 팔의 인벤토리 (inventory) 역할을 합니다. 해당 버전 번호들이 어떻게 기록되는지에 대해서는 그 자체로 하나의 이야기가 있으며, 다음 섹션에서 다룹니다.
이 페이지에는 더 많은 계획이 있습니다. 로봇은 이미 모델을 위해 세 개의 카메라를 탑재하고 있으며, 이를 제어 평면 (control plane)으로 스트리밍하면 원격 세션에서 로봇이 보는 화면을 관절이 보고하는 데이터와 함께 동일한 장소에서 확인할 수 있게 될 것입니다.
인벤토리 (The inventory)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기