SignalK와 함께 Raspberry Pi에서 OpenClaw 실행하기
요약
Raspberry Pi 5 환경에서 OpenClaw 에이전트 게이트웨이와 SignalK를 결합하여 실시간 선박 데이터를 Telegram으로 관리하는 방법을 소개합니다. 설정 시 주의해야 할 gateway.mode 설정, SI 단위 변환 문제, InfluxDB 연동 등 실전 구축 가이드를 제공합니다.
핵심 포인트
- Raspberry Pi 5에서 OpenClaw와 SignalK를 연동한 선박 어시스턴트 구축
- Telegram DM을 통해 실시간 선박 상태 확인 및 데이터 제어 가능
- gateway.mode 설정 누락 시 발생하는 데몬 시작 차단 문제 해결법
- SignalK의 SI 단위 데이터를 에이전트가 처리하도록 하는 주의사항
- 토큰 비용 최적화를 위한 튜닝 및 단계별 가이드 포함
자체 호스팅되는 멀티 채널 에이전트 게이트웨이(agent gateway)인 OpenClaw를 SignalK와 함께 Raspberry Pi 5에서 실행하여, Telegram으로 선박에 DM을 보내고 실시간 선박 데이터를 받아보는 방법을 소개합니다. 제가 시간을 허비했던 함정들은 미리 말씀드리겠습니다. gateway.mode 시작 차단 문제, SI 단위 변환, InfluxDB 없이는 기록이 남지 않는 문제, 그리고 ollama 플러그인과 관련된 Chesterton's-fence 문제입니다. 그 다음으로는 단계별 가이드와 스탠딩 프롬프트(standing prompt)를 낮추며 튜닝한 결과에 대해 다룹니다. 주의사항(gotchas)부터 확인하세요.
제가 찾을 수 있는 범위 내에서는 이 조합에 대한 기존 기록이 없었기에, 시작하기 전에 알아야 할 사항, 실제로 작동하는 설정법, 그리고 계속 실행해 두어도 될 만큼 비용을 저렴하게 만든 토큰 비용(token-cost) 튜닝법까지 모든 내용을 정리했습니다.
빌드 (The build)
OpenClaw는 본인의 하드웨어에서 실행하는 Node/TypeScript 데몬(daemon)이며, 채팅 채널(Telegram, Slack 등)을 통해 LLM 기반 에이전트와 통신합니다. SignalK는 NMEA 2000/0183을 하나의 JSON 모델로 정규화하는 오픈 마린 데이터 서버(open marine data server)입니다. 이 둘을 동일한 Pi에 설치하면, 휴대폰으로 DM을 보내 실시간 선박 상태를 읽을 수 있는 선박 어시스턴트를 얻게 됩니다.
두 서비스는 동일한 기기를 공유하지만 각자의 영역을 유지합니다. OpenClaw는 HTTP를 통해 SignalK를 읽으며, 마린 스택(marine stack) 내부에서 실행되지 않습니다:
Raspberry Pi 5 (8 GB)
┌─────────────────────────────────────────────┐
│ SignalK (Docker) OpenClaw (native) │
...
SignalK는 일반적으로 InfluxDB 및 Grafana와 함께 localhost:3000의 Docker 상에서 이미 실행 중입니다. OpenClaw는 그 옆에 네이티브(native)로 설치됩니다. MCP 서버도, SignalK 내부의 플러그인도 필요하지 않습니다. 에이전트는 단순히 SignalK REST API를 curl로 호출합니다.
주의사항 (Gotchas first)
직접 부딪히기 전까지는 알 수 없는 문제들입니다.
1. gateway.mode 없이는 게이트웨이가 시작되지 않음
설정(config)에 gateway.mode가 누락되어 있으면 데몬이 시작을 거부합니다:
Gateway start blocked: set gateway.mode=local
온보딩 (Onboarding) 과정에서 이 설정이 자동으로 완료됩니다. 하지만 설정을 수동으로 편집하다가 해당 키를 누락하거나, 도구가 설정을 다시 작성하는 경우 OpenClaw는 누락된 키를 의심스럽거나 손상된 설정으로 간주하여 추측하는 대신 시작을 차단합니다. 해결 방법은 명령어 하나면 됩니다:
openclaw config set gateway.mode local
(또는 openclaw onboard --mode local 명령어로 온보딩을 다시 실행하세요.) 자세한 내용은 gateway troubleshooting docs를 참조하십시오.
2. SignalK 값은 SI 단위입니다 — 에이전트가 변환해야 합니다
SignalK는 모든 데이터를 SI 단위로 저장합니다. 원시 데이터(raw read)는 정상적으로 보일 수 있지만, 사람이 보기에는 조용히 잘못된 값입니다:
speed → m/s (노트(knots)로 변환 시 × 1.94384)
angles → radians (도(degrees)로 변환 시 × 57.2958)
temperature→ Kelvin (°C로 변환 시 − 273.15)
...
따라서 environment.wind.speedApparent가 8.5로 반환된다면, 이는 8.5노트가 아니라 8.5 m/s(즉, 약 16.5노트)를 의미합니다. 에이전트에게 변환하도록 지시하지 않으면, 에이전트는 SI 수치를 마치 사람이 사용하는 단위인 것처럼 그대로 보고할 것입니다. 에이전트의 지침(또는 스킬 (skill))에 변환 로직을 포함시키고, 목표 단위를 명확히 명시하십시오.
3. InfluxDB 없이는 이력을 확인할 수 없습니다
SignalK의 REST API는 익명으로 실시간 값을 제공하는 데 문제가 없습니다. 하지만 InfluxDB 기반의 히스토리 플러그인 (history plugin)이 실행 중이지 않으면 히스토리 엔드포인트(history endpoint)는 404 오류를 반환합니다:
$ curl -s http://localhost:3000/signalk/v2/history/values?paths=electrical.batteries.house.stateOfCharge
# signalk-to-influxdb (+ history API)가 설치되어 실행 중이지 않으면 404 반환
이 플러그인이 없으면 에이전트는 실시간 데이터만 다룰 수 있습니다. 즉, "어젯밤 최저 수심이 얼마였나"가 아니라 "지금 수심이 얼마인가"에 대해서만 답할 수 있습니다. 에이전트가 추세(trends)를 바탕으로 추론하기를 원한다면, 먼저 히스토리 플러그인을 구축하십시오. 그렇지 않다면 에이전트의 지침 범위를 현재 시점의 질문으로 제한하십시오.
4. jq는 기본적으로 설치되어 있지 않습니다
Raspberry Pi OS에는 jq가 포함되어 있지 않습니다. 만약 에이전트의 지침이 curl의 결과를 jq로 파이프(pipe) 처리하도록 되어 있다면, 새로 설치한 환경에서는 실패하게 됩니다:
$ curl -s http://localhost:3000/signalk/v1/api/vessels/self/environment/depth/belowTransducer | jq .value
bash: jq: command not found
두 가지 옵션이 있습니다: sudo apt install jq를 실행하거나, 아예 하지 않는 것입니다. 에이전트에게 가공되지 않은 JSON (raw JSON)을 그대로 전달하여 스스로 파싱하게 만드는 것이죠. LLM은 도움 없이도 { "value": 4.2, "timestamp": "…" }와 같은 형식을 읽을 수 있습니다. 저는 가공되지 않은 JSON을 선택했습니다. 배 위에 관리해야 할 구성 요소를 하나라도 줄이기 위해서였습니다.
5. 체스터턴의 울타리 (Chesterton's fence) — ollama 플러그인은 생각보다 더 많은 일을 합니다
저는 ollama 플러그인이 (저에게는 작동하지 않는) 단순한 웹 검색 제공자일 뿐이라고 가정하고 비활성화했습니다. 나중에 로컬 Ollama 모델을 로드하는 데 실패했습니다:
No API provider registered for api: ollama
ollama 플러그인은 Ollama를 단순한 검색 도구가 아닌 **모델 백엔드 (model backend)**로 사용할 수 있게 해주는 Ollama API 런타임 (runtime)도 함께 등록합니다. 플러그인을 비활성화하자 모델의 기반이 되는 백엔드가 사라져 버린 것입니다. 여기서 얻은 교훈은 예전과 같습니다. 플러그인이 제공하는 모든 기능을 알기 전까지는 함부로 비활성화하지 마세요. (다시 활성화하니 로드 문제가 해결되었습니다.)
워크스루 (The walkthrough)
설치 (네이티브)
OpenClaw는 Docker를 지원하지만, 이미 SignalK 자체의 Docker 스택이 실행 중인 Pi에서는 OpenClaw를 네이티브 (native)로 설치하고 자체 데몬 (daemon)을 관리하도록 설정했습니다. 이렇게 하면 해양 관련 컨테이너 (marine containers)를 격리된 상태로 유지하면서 에이전트의 생명 주기 (lifecycle)를 분리할 수 있습니다. 설치 프로그램은 기본적으로 Node 24를 대상으로 하며 (지원 범위는 Node 22.22.3+, 24.15+, 또는 25.9+입니다), 런타임을 자동으로 처리해 줍니다:
curl -fsSL https://openclaw.ai/install.sh | bash
# 또는: npm install -g openclaw@latest
그 다음에는 가이드에 따라 설정을 진행합니다. 인증 (Anthropic API 키 또는 OAuth 로그인), 모델 선택, 그리고 데몬 설정을 안내합니다:
openclaw onboard
openclaw onboard --install-daemon # systemd 사용자 서비스를 설치합니다
헤드리스 (headless) Pi의 경우, 린거링 (lingering)을 활성화하지 않으면 로그아웃 시 사용자 서비스가 종료됩니다:
sudo loginctl enable-linger "$USER"
이 설정이 재부팅 후에도 서비스가 유지되도록 만듭니다. 자세한 내용은 설치 문서를 참조하세요.
Telegram
@BotFather를 통해 봇을 만든 다음, 설정 파일이나 환경 변수 (env)를 통해 OpenClaw에 토큰을 지정하세요:
openclaw config set channels.telegram.botToken "<BOT_TOKEN>"
# 또는 export TELEGRAM_BOT_TOKEN=<BOT_TOKEN>
페어링 (pairing)을 통해 DM (Direct Message)을 제한하여, 봇을 발견한 낯선 사람이 당신의 보트를 조종할 수 없도록 하세요:
// channels.telegram
{ "dmPolicy": "pairing" }
봇에게 DM을 보낸 다음, 봇이 제공하는 코드를 승인하세요 (코드는 한 시간 후에 만료됩니다):
openclaw pairing approve telegram <CODE>
자세한 내용은 Telegram 채널 문서를 참조하세요.
HTTP를 통한 SignalK 읽기 — MCP 불필요
allow_readonly가 켜져 있을 때 SignalK의 REST API는 익명으로 제공되므로, 읽기 작업을 위해 별도의 커스텀 SignalK MCP 서버가 필요하지 않습니다. 에이전트에게 exec 도구를 부여하고 curl을 사용하게 하세요. SignalK의 점 표기법 경로 (dotted path)는 URL 슬래시로 바로 매핑됩니다:
# environment.depth.belowTransducer → .../environment/depth/belowTransducer
curl -s http://localhost:3000/signalk/v1/api/vessels/self/environment/depth/belowTransducer
{ "value": 4.2, "timestamp": "2026-07-27T00:00:00Z", "meta": { "units": "m" } }
한 번의 호출로 전체 서브트리 (subtree)를 가져오세요:
curl -s http://localhost:3000/signalk/v1/api/vessels/self/electrical
단 하나의 전제 조건: exec는 특정 도구 프로필 (tool profiles)에만 존재합니다. 이는 아래의 첫 번째 튜닝 결과에서 다룹니다.
튜닝 결과 1 — 스킬 (skill) vs 에이전트 파일
OpenClaw는 매 턴마다 워크스페이스 파일을 컨텍스트 (context)에 주입합니다: SOUL.md 페르소나 (persona)와 AGENTS.md 지침/메모리 (instruction/memory) 파일입니다. 스킬은 workspace/skills/<name>/SKILL.md에 위치합니다. 스킬의 이름과 설명은 매 턴마다 공지되지만, 실제 본문은 스킬이 실행될 때 필요에 따라 로드됩니다.
그렇다면 SignalK를 읽는 지침은 어디에 속해야 할까요? 항상 켜져 있는 AGENTS.md일까요, 아니면 SKILL.md일까요? 직관적으로는 스킬입니다. 사용할 때만 본문에 대한 비용을 지불하는 것이죠. 저는 두 방식 모두에 대해 턴당 프롬프트 토큰 (prompt tokens)을 측정했습니다:
| 턴 (Turn) | AGENTS.md 내 포함 시 (always-on) | 스킬로서 사용 시 (on-demand) | Δ |
|---|---|---|---|
| 도구 미사용 질문 (No-tool question) | 13,808 | 13,380 | −428 (스킬 승리) |
| ... |
직관에 어긋나지만 일관된 결과입니다: 이 도메인이 업무의 전부인 에이전트의 경우, 항상 켜져 있는 (always-on) 에이전트 파일이 약간 더 저렴합니다. 스킬의 _설명 (description)_은 여전히 매 턴 (다른 모든 스킬과 함께) 컨텍스트 (context)에 머물러 있으며, 도메인 관련 턴에서 스킬의 _본문 (body)_을 로드하는 비용이 그것이 대체했던 아주 작은 always-on 스니펫 (snippet)보다 더 많이 들기 때문입니다. 스킬 방식은 대부분의 턴이 해당 도메인을 건드리지 않을 때만 승리합니다. 하지만 보트 에이전트의 경우, 대부분의 턴이 해당 도메인을 건드립니다.
(이는 다른 프레임워크에서 겪었던 것과 동일한 always-on 대 조건부 (conditional) 분할 방식입니다 — 아래 관련 포스트를 참조하세요.)
튜닝 결과 2 — 약 14k의 상시 프롬프트 (standing prompt) 줄이기
매 턴, 도구를 사용하지 않는 질문일지라도 약 14k 토큰의 상시 프롬프트 (standing prompt)가 수반됩니다: 기본 서문 (base preamble) + 도구 스키마 (tool schemas) + 워크스페이스 파일 (workspace files). 이는 범용 게이트웨이 (gateway)의 구조적 특성입니다. 이를 줄이기 위해 시도했던 세 가지 방법을 실제 효과가 있었던 순서대로 나열합니다.
효과가 있었던 방법 — 도구 프로필 (tool profile)
OpenClaw는 tools.profile을 통해 도구를 제한합니다: minimal (session_status만 포함), coding (파일 시스템 + 런타임/exec + 웹 + 기타), messaging, 그리고 full. coding 프로필만이 exec와 파일 시스템을 모두 허용합니다 — 이것이 SignalK-over-curl 방식에 해당 프로필이 필요한 이유입니다.
하지만 coding 프로필은 보트 에이전트가 절대 호출하지 않는 수많은 개발 도구 스키마 (dev-tool schemas)를 함께 끌어옵니다. 만약 셸 (shell) + 파일 읽기/쓰기만 필요하다면, minimal로 낮춘 뒤 딱 그 두 그룹만 가산적으로 다시 허용하십시오:
// tools
{
"profile": "minimal",
...
group:runtime은 exec/프로세스이며, group:fs는 읽기/쓰기/편집입니다. 이를 통해 기능적 손실 없이 **턴당 약 5,000 토큰 (~25%)**을 절감했습니다 — 이는 에이전트가 전혀 사용하지 않는 도구들을 위한 순수 스키마 (schema) 비용이었습니다. tool-profile 문서를 참조하세요.
효과가 없었던 방법 — 플러그인 비활성화
저는 로드된 플러그인들 (browser, canvas, phone-control, talk-voice…)이 프롬프트를 팽창시키고 있다고 가정했습니다. 하지만 그중 4개를 비활성화해도 약 270 토큰 정도만 절약되었을 뿐 — 아무런 효과가 없었습니다.
이유: 도구 스키마 (tool schemas)는 플러그인의 상태 (plugin state)가 아니라 도구 정책 (tool policy)에 의해 제어되기 때문입니다. 일단 minimal 프로필에서 특정 도구를 제외하면, 해당 플러그인이 로드되든 아니든 그 스키마는 이미 사라진 상태입니다. 즉:
프로필이 이미 도구 사용을 거부하고 있다면, 플러그인을 비활성화하더라도 프롬프트 토큰 (prompt tokens)을 절약할 수 없습니다.
(그래도 사용하지 않는 플러그인은 비활성화하세요. Raspberry Pi의 RAM, CPU 및 부팅 시간을 위해서 말이죠. 다만 토큰 절약 효과는 기대하지 마세요.)
진짜 해결책 — 토큰 수가 아닌 캐싱 (caching)
기존 프롬프트 비용은 오직 '콜드 (cold)' 턴에서만 전액 부과됩니다. OpenClaw는 안정적인 접두사 (stable prefix)에 Anthropic의 cache_control을 자동으로 주입하므로, '웜 (warm)' 턴에서는 약 14k 토큰의 약 10% 정도를 캐시에서 읽어옵니다. 제가 한 세션 동안 관찰한 결과는 다음과 같습니다. 콜드 턴에서는 약 14k 토큰을 캐시에 '썼고 (wrote)', 동일 세션 내의 다음 턴에서는 캐시에서 약 14k 토큰을 '읽었으며 (read)', 단 ~150 토큰만을 '썼습니다 (wrote)'.
간헐적인 사용 — 즉, 보트에 DM을 보내는 방식처럼 메시지 간격이 몇 분씩 벌어지는 경우 — 기본 5분 캐시 TTL (Time To Live) 설정 때문에 계속 콜드 스타트가 발생합니다. 이를 늘리세요:
// 긴 캐시 유지 시간 → 1시간 TTL
{ "cacheRetention": "long" }
한 가지 버전 관련 주의사항: OpenClaw는 하트비트 (heartbeat) 파일의 빈번한 변경이 캐시된 접두사를 깨뜨리지 않도록, 안정적인 컨텍스트 파일들을 하트비트 파일보다 앞 순서로 배치하는 데 주의를 기울입니다. 하지만 이전 빌드에는 메시지당 값이 캐시 블록 '내부'에 위치하여 매 턴마다 전체 접두사를 다시 쓰는 캐시 파괴 (cache-busting) 버그가 있었습니다. 만약
에이전트가 SignalK를 읽을 수 있게 되면, 자연스러운 다음 단계는 문제가 발생했을 때만 말을 하는 예약된 체크(scheduled check)입니다. OpenClaw의 cron은 결정론적인 --trigger-script 게이트를 지원합니다. 이 스크립트는 SignalK를 검사하여(any non-normal 알림, 또는 배터리/수심/탱크 임계값), { fire, message?, state? }를 반환합니다. 에이전트는 fire가 true일 때만 Telegram 알림을 작성하며, state를 통해 중복을 제거(de-duped)하므로 매 틱(tick)마다 알리는 것이 아니라 _변화(change)_가 있을 때만 알림을 보냅니다. 모든 것이 정상일 때는 침묵하는 감시 장치입니다. 이에 대한 자세한 내용은 별도의 포스트로 다룰 예정이며, cron 문서에 그 형태가 나와 있습니다.
마치며
이 프로젝트는 모든 것이 전기식인 차터용 카타마란(catamaran)을 위한 AI ops 레이어를 구축하는 과정에서 나왔습니다. 이곳에서는 "현재 수심이 얼마인가요?"라는 질문에 Telegram DM 하나로 답을 얻을 수 있어야 하며, 상시 실행되는 프롬프트(standing prompt)는 Raspberry Pi에서 계속 돌아갈 수 있을 만큼 저렴해야 합니다. 만약 직접 이를 구축하신다면, 북마크할 가치가 있는 두 가지 참조 자료는 OpenClaw 문서와 signalk.org입니다. 나머지는 위에 언급한 여섯 줄의 주의 사항(gotchas)이 전부입니다.
관련 글: 왜 당신의 에이전트는 스킬 바디(skill body)는 무시하면서 시스템 프롬프트(system prompt)는 따르는가 — 튜닝 결과 뒤에 숨겨진 상시 실행(always-on) 대 조건부 실행(conditional)의 분리; 그리고 96%의 토큰 절약에도 불구하고 왜 우리가 이름이 지정된 MCP 도구를 유지했는가 — 보트 에이전트를 위한 토큰 대 신뢰성(reliability)의 트레이드오프.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기