@rainbow-robotics/muscat-plugin-kit 1.10.3-dev.2 → 1.10.3-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +1059 -0
- package/README.md +927 -150
- package/cli/README.md +1 -1
- package/cli/bin/muscat-pk.mjs +1 -1
- package/cli/src/build.mjs +9 -13
- package/cli/src/dev.mjs +4 -2
- package/cli/src/mcp.mjs +17 -3
- package/cli/src/muscatLocal.mjs +2 -2
- package/cli/src/pluginDevServer.mjs +1 -1
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-JZmvrBu2.js → Color-YHDXOIA2-Cp8LHsF-.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CHh5amiz.js → Docs-B2ox3o_G.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DdU-wsGN.js → Docs-By11aG_M.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DjjxchlZ.js → Docs-Kg1Im4XT.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-GWKBJz_w.js → Docs-LeW_BIsw.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DrMHaIyt.js → Docs-UJwiVwO2.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DXJ25Kqq.js → Docs-ik2AhpV0.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-DNcXzd9o.js → DocsRenderer-CFRXHY34-DPFneFUi.js} +1 -1
- package/cli/storybook-dist/assets/{entry-preview-Du01HPfn.js → entry-preview-BTw3Jh8E.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-DFBATQRS.js → iframe-BqPQIkPQ.js} +2 -2
- package/cli/storybook-dist/assets/{index-DG8XddNr.js → index-k_VkBflK.js} +3 -3
- package/cli/storybook-dist/assets/{preview-donYBfDJ.js → preview-C3ctYQQj.js} +2 -2
- package/cli/storybook-dist/assets/preview-Cog7BnbD.js +3 -0
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/README.md +2 -3
- package/cli/templates/common/requirements.txt +3 -3
- package/cli/templates/cpp/cpp/CMakeLists.txt +1 -1
- package/cli/templates/readme-cpp/README.md +3 -4
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +1 -1
- package/package.json +5 -4
- package/cli/storybook-dist/assets/preview-e0NAgyy_.js +0 -3
package/README.ko.md
ADDED
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
# @rainbow-robotics/muscat-plugin-kit
|
|
2
|
+
|
|
3
|
+
Muscat 플러그인 개발 CLI(`muscat-pk`)입니다. 프로젝트 생성, 개발 서버(UI + 백엔드), 빌드/패키징, 그리고 플러그인 작성에 필요한 SDK 사용법을 다룹니다.
|
|
4
|
+
|
|
5
|
+
[English (README.md)](./README.md)
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
1. [설치](#1-설치)
|
|
10
|
+
2. [빠른 시작](#2-빠른-시작)
|
|
11
|
+
3. [명령어](#3-명령어)
|
|
12
|
+
4. [프로젝트 구조](#4-프로젝트-구조)
|
|
13
|
+
5. [manifest.json](#5-manifestjson)
|
|
14
|
+
6. [플러그인 UI SDK (`useRb`)](#6-플러그인-ui-sdk-userb)
|
|
15
|
+
7. [Python 플러그인 SDK](#7-python-플러그인-sdk)
|
|
16
|
+
8. [다른 백엔드 런타임 (C++ / ROS 2)](#8-다른-백엔드-런타임-c--ros-2)
|
|
17
|
+
9. [개발 환경](#9-개발-환경)
|
|
18
|
+
10. [스타일](#10-스타일)
|
|
19
|
+
11. [AI 코딩 에이전트 설정 (`muscat-pk mcp`)](#11-ai-코딩-에이전트-설정-muscat-pk-mcp)
|
|
20
|
+
12. [환경 변수](#12-환경-변수)
|
|
21
|
+
13. [문제 해결](#13-문제-해결)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. 설치
|
|
26
|
+
|
|
27
|
+
### npm (Node.js 20 이상)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g @rainbow-robotics/muscat-plugin-kit # 최신 stable
|
|
31
|
+
npm install -g @rainbow-robotics/muscat-plugin-kit@dev # dev 채널
|
|
32
|
+
npm install -g @rainbow-robotics/muscat-plugin-kit@1.2.3 # 특정 버전
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 설치 스크립트 (Node.js 없어도 됨)
|
|
36
|
+
|
|
37
|
+
macOS / Linux / WSL:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | sh
|
|
41
|
+
curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | MUSCAT_PK_TAG=dev sh
|
|
42
|
+
curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | MUSCAT_PK_TAG=1.2.3 sh
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Windows PowerShell:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
irm https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.ps1 | iex
|
|
49
|
+
$env:MUSCAT_PK_TAG="dev"; irm https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.ps1 | iex
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Node.js 20 이상이 있으면 npm으로 설치하고, 없으면 단일 실행 바이너리를 내려받습니다.
|
|
53
|
+
|
|
54
|
+
### 바이너리 직접 다운로드
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/v{version}/{binary}
|
|
58
|
+
https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/stable/{binary}
|
|
59
|
+
https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/dev/{binary}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| OS | 파일 |
|
|
63
|
+
|---|---|
|
|
64
|
+
| Linux x64 | `muscat-pk-linux-x64` |
|
|
65
|
+
| macOS ARM64 | `muscat-pk-darwin-arm64` |
|
|
66
|
+
| macOS x64 | `muscat-pk-darwin-x64` |
|
|
67
|
+
| Windows x64 | `muscat-pk-win-x64.exe` |
|
|
68
|
+
|
|
69
|
+
채널별 최신 버전: `.../muscat-plugin-kit/latest-version.txt` (stable), `.../muscat-plugin-kit/dev-version.txt` (dev).
|
|
70
|
+
|
|
71
|
+
### 필요한 도구
|
|
72
|
+
|
|
73
|
+
| 도구 | 용도 |
|
|
74
|
+
|---|---|
|
|
75
|
+
| Node.js 20 이상 (또는 바이너리) | CLI, UI 빌드 |
|
|
76
|
+
| Docker | `none`을 제외한 모든 백엔드 런타임 (`dev:back`, `dev`, `build`) |
|
|
77
|
+
| `uv` 또는 Python 3.12 (선택) | Python 린트용 IDE 전용 `.venv` |
|
|
78
|
+
|
|
79
|
+
### 삭제
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
muscat-pk uninstall # npm·바이너리 설치 모두 가능
|
|
83
|
+
npm uninstall -g @rainbow-robotics/muscat-plugin-kit # 수동, npm
|
|
84
|
+
sudo rm -f /usr/local/bin/muscat-pk # 수동, 바이너리
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 2. 빠른 시작
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
muscat-pk new my-plugin # 프로젝트 생성 (작성자, 이메일, 설명, 타입, 패키지 매니저를 물어봄)
|
|
93
|
+
cd my-plugin
|
|
94
|
+
muscat-pk dev # UI 프리뷰 + 백엔드 동시 실행
|
|
95
|
+
muscat-pk build # dist/ + 배포용 ZIP
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
만들어진 ZIP은 로봇 Muscat의 플러그인 페이지에서 설치합니다.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 3. 명령어
|
|
103
|
+
|
|
104
|
+
| 명령 | 설명 |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `muscat-pk new [dir]` | 플러그인 프로젝트 생성 |
|
|
107
|
+
| `muscat-pk type change` | 백엔드 타입 변경 |
|
|
108
|
+
| `muscat-pk dev` | 로컬 Muscat 런타임 + `dev:ui` + `dev:back` |
|
|
109
|
+
| `muscat-pk dev:ui` | mini-Muscat(preview-host)에서 UI 프리뷰 |
|
|
110
|
+
| `muscat-pk dev:back` | 백엔드만 Docker에서 실행 (hot reload) |
|
|
111
|
+
| `muscat-pk build` | 빌드, Python 의존성 lock, wheel 동봉, ZIP |
|
|
112
|
+
| `muscat-pk update` | kit, `plugin-ui`, `preview-host` 업데이트 |
|
|
113
|
+
| `muscat-pk sdk <list\|update [version]>` | `rainbow-rb-sdk` 버전 관리 |
|
|
114
|
+
| `muscat-pk storybook` | 번들된 rb-components Storybook 보기 |
|
|
115
|
+
| `muscat-pk mcp` | AI 코딩 에이전트용 MCP 서버 |
|
|
116
|
+
| `muscat-pk uninstall` | CLI 삭제 |
|
|
117
|
+
| `muscat-pk version` | 실행 중인 버전 출력 (`-v`, `--version`) |
|
|
118
|
+
| `muscat-pk completion [shell]` | 셸 자동완성 스크립트 출력 |
|
|
119
|
+
|
|
120
|
+
`muscat-pk <command> --help`로 각 명령의 옵션을 볼 수 있습니다.
|
|
121
|
+
|
|
122
|
+
### 3.1 `new`
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
muscat-pk new [dir] [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun]
|
|
126
|
+
[--author <name>] [--email <email>] [--description <text>]
|
|
127
|
+
[--yes] [--no-install]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| 옵션 | 의미 |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `dir` | 생성할 디렉터리 (비어 있어야 함). 생략하면 물어봄 |
|
|
133
|
+
| `--type` | 백엔드 타입: `python` (기본), `cpp`, `ros2` (rclpy), `ros2-cpp` (rclcpp) |
|
|
134
|
+
| `--pm` | 프론트 패키지 매니저. Yarn 워크스페이스 안이면 기본 `yarn`, 아니면 `npm` |
|
|
135
|
+
| `--author` / `--email` / `--description` | manifest 필드 (email은 manifest 필수값) |
|
|
136
|
+
| `--yes` | 질문 없이 진행. 지정 안 한 값은 기본값 |
|
|
137
|
+
| `--no-install` | `npm install` 생략 |
|
|
138
|
+
|
|
139
|
+
Python 프로젝트는 IDE 전용 `.venv`도 만들어집니다 (9.6 참고).
|
|
140
|
+
|
|
141
|
+
### 3.2 `type change`
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
muscat-pk type change [--to python|cpp|ros2|ros2-cpp] [--readme]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`manifest.json`의 `runtime`을 바꾸고 새 백엔드 파일을 추가합니다. 이전 타입의 파일(`cpp/`, `ros2_ws/` 등)은 지우지 않습니다. `--readme`를 주면 README도 새 타입용으로 교체합니다.
|
|
148
|
+
|
|
149
|
+
### 3.3 `dev`, `dev:ui`, `dev:back`
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
muscat-pk dev [--robot-ip <ip>] [--keep-id]
|
|
153
|
+
muscat-pk dev:ui [--robot-ip <ip>] [--port <n>] [--keep-id]
|
|
154
|
+
muscat-pk dev:back [--keep-id]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| 명령 | 실행되는 것 |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `dev` | 로봇이 없으면 로컬 Muscat 런타임, 그리고 `dev:ui` + `dev:back` |
|
|
160
|
+
| `dev:ui` | 플러그인 Vite 개발 서버 + preview-host `http://localhost:4180/plugin-dev` (사용 중이면 다음 빈 포트) |
|
|
161
|
+
| `dev:back` | 로봇 환경과 같은 Docker 컨테이너에서 백엔드 실행. Python은 `py/**/*.py`가 바뀌면 재시작 (watchfiles) |
|
|
162
|
+
|
|
163
|
+
- 로봇 IP는 `--robot-ip`, `MUSCAT_ROBOT_HOST`, 또는 프롬프트 입력 순입니다 (비우면 로컬).
|
|
164
|
+
- 원격 로봇에 붙으면 플러그인이 dev id로 실행됩니다 (9.3 참고). `--keep-id`는 manifest id를 그대로 씁니다.
|
|
165
|
+
- 시작 전에 manifest를 검증하고, 선언하지 않은 포트를 열면 백엔드를 멈춥니다 (5.6 참고).
|
|
166
|
+
|
|
167
|
+
### 3.4 `build`
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
muscat-pk build [--arch amd64|arm64|both]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
순서:
|
|
174
|
+
|
|
175
|
+
1. `manifest.json`의 새 버전을 물어봅니다 (비우면 그대로).
|
|
176
|
+
2. manifest를 검증합니다.
|
|
177
|
+
3. `contributes[].ui` 엔트리마다 `vite build` → `dist/<entry>.js`.
|
|
178
|
+
4. 백엔드:
|
|
179
|
+
- python: `requirements.txt`에서 버전이 없는 줄을 PyPI 최신 버전으로 고정하고, `dist/requirements.lock`(`uv pip compile`)을 만들고, arch별 wheel을 `dist/wheels/`에 넣습니다.
|
|
180
|
+
- cpp: Docker에서 CMake 빌드 → `dist/<arch>/<entry>` + `dist/<arch>/lib/librb_sdk.so`.
|
|
181
|
+
- ros2: Docker에서 colcon 빌드 → `dist/ros2_ws/install-<arch>/`.
|
|
182
|
+
5. `dist/`를 프로젝트 루트의 `<프로젝트 디렉터리>-<version>.zip`으로 묶습니다.
|
|
183
|
+
|
|
184
|
+
| 옵션 | 의미 |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `--arch` | 대상 아키텍처. TTY면 물어보고, 아니면 `both` |
|
|
187
|
+
|
|
188
|
+
### 3.5 `update`
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
muscat-pk update # 현재 프로젝트만 (로컬 설치가 있을 때)
|
|
192
|
+
muscat-pk update --global # 현재 프로젝트 + 전역 CLI
|
|
193
|
+
muscat-pk update dev # dev 채널 따라가기
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
| 실행 위치 | 업데이트 대상 |
|
|
197
|
+
|---|---|
|
|
198
|
+
| 로컬 설치가 있는 프로젝트 안 | 그 프로젝트의 `muscat-plugin-kit` + `plugin-ui` |
|
|
199
|
+
| 위와 같고 `--global` | 위 + 전역 CLI |
|
|
200
|
+
| 그 밖 | 전역 CLI (프로젝트 안이면 로컬 의존성도) |
|
|
201
|
+
|
|
202
|
+
업데이트 후 제거된 API를 아직 쓰는 UI 코드를 목록으로 보여 줍니다. 목록대로 고치거나 `muscat-pk mcp`의 `check_project`로 확인하세요.
|
|
203
|
+
|
|
204
|
+
### 3.6 `sdk`
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
muscat-pk sdk list # 배포된 rainbow-rb-sdk 버전 목록
|
|
208
|
+
muscat-pk sdk update [version] # 생략하면 최신
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- python: `requirements.txt`의 `rainbow-rb-*==` 핀을 모두 같은 버전으로 바꿉니다 (서로 정확히 같은 버전을 요구함).
|
|
212
|
+
- C++ (`cpp`, `ros2-cpp`): `CMakeLists.txt`의 `find_package(rb_sdk <ver> EXACT REQUIRED)`를 바꿉니다.
|
|
213
|
+
|
|
214
|
+
### 3.7 `storybook`, `mcp`, `completion`, `version`
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
muscat-pk storybook [--port 6007] [--rebuild]
|
|
218
|
+
muscat-pk mcp [--install codex]
|
|
219
|
+
muscat-pk completion [bash|zsh|fish|powershell]
|
|
220
|
+
muscat-pk version
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`completion`은 셸 Tab 자동완성 스크립트를 출력합니다. 한 번 등록하면 `muscat-pk d<Tab>`에서 `dev`, `dev:ui`, `dev:back`이 후보로 나옵니다. 스크립트를 출력만 하므로 아래처럼 셸 rc 파일에 붙여 넣어야 합니다. `[shell]`은 `bash`, `zsh`, `fish`, `powershell` 중 하나이고 생략하면 `$SHELL`을 따릅니다. Windows `cmd.exe`는 자동완성을 지원하지 않으니 PowerShell, Git Bash, WSL을 쓰세요. 편의 기능이라 쓰지 않아도 됩니다.
|
|
224
|
+
|
|
225
|
+
| OS / 셸 | 자동완성 설정 |
|
|
226
|
+
|---|---|
|
|
227
|
+
| macOS (zsh) | `muscat-pk completion zsh >> ~/.zshrc && source ~/.zshrc` |
|
|
228
|
+
| Linux (bash) | `muscat-pk completion bash >> ~/.bashrc && source ~/.bashrc` |
|
|
229
|
+
| fish | `muscat-pk completion fish > ~/.config/fish/completions/muscat-pk.fish` |
|
|
230
|
+
| PowerShell | `muscat-pk completion powershell >> $PROFILE; . $PROFILE` |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 4. 프로젝트 구조
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
my-plugin/
|
|
238
|
+
├─ ui/
|
|
239
|
+
│ ├─ content.tsx # UI 엔트리 (contributes[].ui 하나당 파일 하나), `export default App`
|
|
240
|
+
│ └─ style.css # 모든 엔트리에 자동 로드
|
|
241
|
+
├─ py/main.py # python 백엔드 엔트리 (runtime.entry)
|
|
242
|
+
├─ cpp/ # cpp 백엔드 (CMake) — cpp 타입
|
|
243
|
+
├─ ros2_ws/ # colcon 워크스페이스 — ros2 / ros2-cpp 타입
|
|
244
|
+
├─ assets/icon.svg
|
|
245
|
+
├─ manifest.json
|
|
246
|
+
├─ requirements.txt # python 의존성 (빌드 시 버전 고정)
|
|
247
|
+
├─ package.json # UI 의존성 (배포물에 포함 안 됨)
|
|
248
|
+
├─ vite.config.ts # muscat() 프리셋 필수
|
|
249
|
+
└─ README.md # dist/에 복사됨
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`vite.config.ts`:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { defineConfig } from "vite";
|
|
256
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
257
|
+
import { muscat } from "@rainbow-robotics/muscat-plugin-kit/vite";
|
|
258
|
+
|
|
259
|
+
export default defineConfig({ plugins: [tailwindcss(), muscat()] });
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
ZIP에는 플러그인 자체 코드와 에셋만 들어갑니다. `@rainbow-robotics/plugin-ui`, `react`, `react-dom`은 호스트의 import map이 제공합니다.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 5. manifest.json
|
|
267
|
+
|
|
268
|
+
`muscat-pk dev`, `dev:back`, `build`가 검증합니다.
|
|
269
|
+
|
|
270
|
+
### 5.1 전체 예시
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"id": "com.example.my-plugin",
|
|
275
|
+
"name": "My Plugin",
|
|
276
|
+
"version": "1.0.0",
|
|
277
|
+
"minMuscatVersion": "1.0.0",
|
|
278
|
+
"author": "Kim",
|
|
279
|
+
"email": "kim@example.com",
|
|
280
|
+
"company": "Example Inc.",
|
|
281
|
+
"description": { "en": "Gripper helper", "ko": "그리퍼 도우미" },
|
|
282
|
+
"icons": "assets/icon.svg",
|
|
283
|
+
"targetModels": "all",
|
|
284
|
+
"targetCategorys": ["manipulate"],
|
|
285
|
+
"contributes": [
|
|
286
|
+
{ "point": "toolbar", "icon": "assets/icon.svg", "label": { "en": "My Plugin", "ko": "내 플러그인" }, "ui": "content", "opens": "popover" }
|
|
287
|
+
],
|
|
288
|
+
"variables": [
|
|
289
|
+
{ "name": "RBP_grip_force", "type": "number", "init": 0.5, "description": { "en": "Gripper force (0–1)", "ko": "그리퍼 힘 (0~1)" } }
|
|
290
|
+
],
|
|
291
|
+
"ports": [28600],
|
|
292
|
+
"runtime": { "type": "python", "entry": "main.py" }
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 5.2 최상위 필드
|
|
297
|
+
|
|
298
|
+
| 필드 | 필수 | 규칙 |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| `id` | 예 | 역도메인 형식: 소문자·숫자로 된 부분을 `.`으로 잇고, 부분 안에서는 `-` 하나로만 연결, 각 부분은 문자로 시작 (`com.example.my-plugin`). `_` 불가 |
|
|
301
|
+
| `name`, `version`, `minMuscatVersion` | 예 | 빈 문자열 불가 |
|
|
302
|
+
| `author`, `email` | 예 | 빈 문자열 불가 |
|
|
303
|
+
| `company` | 아니오 | 문자열 |
|
|
304
|
+
| `description` | 예 | 문자열 또는 다국어 객체 |
|
|
305
|
+
| `icons` | 예 | SVG 경로 문자열, 또는 PNG/JPG면 `{ "16", "48", "128" }` (세 개 모두) |
|
|
306
|
+
| `targetModels` / `targetCategorys` | 둘 중 하나 이상 | `"all"` 또는 비어 있지 않은 배열. 카테고리: `manipulate`, `amr`, `rby1`, `vision` |
|
|
307
|
+
| `contributes` | 예 | 5.3 |
|
|
308
|
+
| `variables` | 아니오 | 5.5 |
|
|
309
|
+
| `ports` | 아니오 | 5.6 |
|
|
310
|
+
| `runtime` | 예 | 5.7 |
|
|
311
|
+
|
|
312
|
+
다국어 객체: `en` 필수, 허용 키 `ko`, `en`, `ja`, `zh`, `vi`, `th`, `es`.
|
|
313
|
+
|
|
314
|
+
### 5.3 `contributes`
|
|
315
|
+
|
|
316
|
+
| 필드 | 규칙 |
|
|
317
|
+
|---|---|
|
|
318
|
+
| `point` | `toolbar`, `lnb`, `move.panel`, `program.panel`, `program.monitor` |
|
|
319
|
+
| `ui` | `ui/` 아래 엔트리 이름, 확장자 없이 (기본 `content`). 엔트리끼리 중복 불가 |
|
|
320
|
+
| `label` | 필수. 문자열 또는 다국어 객체 |
|
|
321
|
+
| `icon` | 아이콘 경로 |
|
|
322
|
+
| `opens` | toolbar 전용: `popover`, `panel`, `page` |
|
|
323
|
+
| `commandInfo` | `program.panel`이면 필수 (5.4) |
|
|
324
|
+
|
|
325
|
+
`toolbar` 항목이 최소 하나 있어야 합니다.
|
|
326
|
+
|
|
327
|
+
| point | 표시 위치 |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `toolbar` | 헤더 툴바 버튼. popover / panel / page로 열림 |
|
|
330
|
+
| `lnb` | 왼쪽 내비게이션 |
|
|
331
|
+
| `move.panel` | Move 페이지 패널 |
|
|
332
|
+
| `program.panel` | Program 페이지 스텝 트리에 추가되는 커스텀 명령. `ui`는 그 스텝의 편집 폼 |
|
|
333
|
+
| `program.monitor` | Program 페이지 모니터 패널의 탭 (General / Digital I/O / Monitor / Log 옆). task별로 렌더링 |
|
|
334
|
+
|
|
335
|
+
### 5.4 `commandInfo` (`program.panel`)
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"point": "program.panel",
|
|
340
|
+
"ui": "program",
|
|
341
|
+
"label": { "en": "My Step", "ko": "내 스텝" },
|
|
342
|
+
"commandInfo": {
|
|
343
|
+
"name": "My Step",
|
|
344
|
+
"icon": "assets/icon.svg",
|
|
345
|
+
"funcName": "rb_program_sdk.plugin_execute",
|
|
346
|
+
"args": { "message": "hello" }
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
| 필드 | 타입 | 의미 |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `name` | string, 필수 | 트리에 표시되는 명령 이름 |
|
|
354
|
+
| `icon` | string, 필수 | 아이콘 경로 |
|
|
355
|
+
| `iconClassName` | string | 아이콘 추가 class |
|
|
356
|
+
| `funcName` | string | 실행기가 호출하는 함수 (Python 백엔드는 `rb_program_sdk.plugin_execute`) |
|
|
357
|
+
| `args` | object | 스텝 기본 인자. `plugin_id`와 함께 백엔드로 전달 |
|
|
358
|
+
| `disabled`, `syncGroupId`, `disableCopy`, `disableDelete`, `requireChildren`, `requireParent` | boolean | 트리 편집 규칙 |
|
|
359
|
+
| `allowChildren`, `allowParents`, `notAllowChildren` | string[] | 허용/금지되는 부모·자식 명령 |
|
|
360
|
+
| `childrenCommand` | string | 자식으로 자동 생성할 명령 |
|
|
361
|
+
|
|
362
|
+
### 5.5 `variables` (PyFM)
|
|
363
|
+
|
|
364
|
+
플러그인이 PyFM 프로그램에 노출할 변수입니다. Program 페이지의 변수 선택기에는 여기 선언한 변수만 나옵니다.
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{ "name": "RBP_target_pose", "type": "array", "init": [0, 0, 0], "description": { "en": "Target pose", "ko": "목표 자세" } }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
| 필드 | 필수 | 규칙 |
|
|
371
|
+
|---|---|---|
|
|
372
|
+
| `name` | 예 | `RBP_` + 영문자/숫자/밑줄. 플러그인 안에서 중복 불가 |
|
|
373
|
+
| `type` | 예 | `number`, `string`, `boolean`, `array` |
|
|
374
|
+
| `init` | 예 | `type`에 맞는 초기값 |
|
|
375
|
+
| `description` | 아니오 | 문자열 또는 다국어 객체. 없으면 UI에 설명이 안 나오므로 적는 것을 권장 |
|
|
376
|
+
|
|
377
|
+
### 5.6 `ports`
|
|
378
|
+
|
|
379
|
+
TCP 포트를 여는 백엔드는 모든 포트를 선언해야 합니다:
|
|
380
|
+
|
|
381
|
+
```json
|
|
382
|
+
{ "ports": [28600, 28601] }
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
| 규칙 | 검사 주체 |
|
|
386
|
+
|---|---|
|
|
387
|
+
| `28600-28999`만, `number[]` | manifest 검증 |
|
|
388
|
+
| 예약 포트 금지: `502`, `2222`, `4840`, `5170-5179`, `8000-8999`, `10000`, `30004`, `34964`, `44818`, `50000-50005`, `50051` | manifest 검증 |
|
|
389
|
+
| 열린 포트는 전부 선언돼야 함 | `dev` / `dev:back` 런타임 모니터가 선언 안 된 포트 발견 즉시 백엔드 중지 |
|
|
390
|
+
| 활성 플러그인끼리 포트 중복 불가 | 로봇, 설치 시 (HTTP 409) |
|
|
391
|
+
|
|
392
|
+
kit 자체 개발용 포트(gateway `10000`, Zenoh dev peer 포트)는 검사에서 제외됩니다. `muscat-pk dev`에서는 선언한 포트가 `127.0.0.1`에 노출되며, 호스트에서 이미 쓰는 포트는 경고 후 건너뜁니다.
|
|
393
|
+
|
|
394
|
+
### 5.7 `runtime`
|
|
395
|
+
|
|
396
|
+
| runtime | 소스 | 필수 필드 |
|
|
397
|
+
|---|---|---|
|
|
398
|
+
| `{ "type": "none" }` | — | UI 전용 플러그인 |
|
|
399
|
+
| `{ "type": "python", "entry": "main.py" }` | `py/` | `entry` |
|
|
400
|
+
| `{ "type": "cpp", "entry": "<executable>" }` | `cpp/` | `entry` = 빌드된 실행 파일 이름 |
|
|
401
|
+
| `{ "type": "ros2", "rosClient": "rclpy", "package": "<pkg>", "launch": "<pkg>_node" }` | `ros2_ws/` | `rosClient`, `package`, `launch` |
|
|
402
|
+
| `{ "type": "ros2", "rosClient": "rclcpp", "package": "<pkg>", "launch": "<pkg>_node" }` | `ros2_ws/` | 동일 |
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 6. 플러그인 UI SDK (`useRb`)
|
|
407
|
+
|
|
408
|
+
패키지 `@rainbow-robotics/plugin-ui`:
|
|
409
|
+
|
|
410
|
+
| import 경로 | 내용 |
|
|
411
|
+
|---|---|
|
|
412
|
+
| `@rainbow-robotics/plugin-ui/react` | Muscat 컴포넌트(`RB*`), `useRb`, `useRbFormData`, `useRbSave`, `useRbStorage`, `createRbStore` |
|
|
413
|
+
| `@rainbow-robotics/plugin-ui/sdk` | 컴포넌트를 뺀 같은 훅들, `RbSdk` / `RbStore` / `RbSetState` 타입 |
|
|
414
|
+
|
|
415
|
+
### 6.1 엔트리 컴포넌트
|
|
416
|
+
|
|
417
|
+
`ui/` 최상위의 각 `*.tsx`는 React 컴포넌트를 default export 합니다. 호스트가 이를 마운트하고 context로 `rb`를 넘겨 줍니다.
|
|
418
|
+
|
|
419
|
+
```tsx
|
|
420
|
+
import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
|
|
421
|
+
import { RBLabelButton } from "@rainbow-robotics/plugin-ui/react";
|
|
422
|
+
|
|
423
|
+
export default function App() {
|
|
424
|
+
const rb = useRb();
|
|
425
|
+
return <RBLabelButton onClick={() => rb.action("grip", { width: 20 })}>Grip</RBLabelButton>;
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
네이티브 `<button>`, `<input>`, `<select>`, `<textarea>`, `<table>`, `<dialog>` 대신 plugin-ui 컴포넌트를 씁니다. 대응표: button → `RBLabelButton` / `RBIconButton`, 텍스트 입력 → `RBTextInput` / `RBTextField`, 숫자 → `RBSideNumberInput`, select → `RBSelectBox` + `Option`, checkbox → `RBCheckBox`, switch → `RBSwitch`, slider → `RBSlider`, table → `RBTable`, modal → `RBModal`, tabs → `RBTabs`. props와 예시는 `muscat-pk storybook`, `muscat-pk mcp`에서 확인합니다.
|
|
430
|
+
|
|
431
|
+
### 6.2 백엔드와 메시지 주고받기
|
|
432
|
+
|
|
433
|
+
| UI | 백엔드 (Python) | 패턴 |
|
|
434
|
+
|---|---|---|
|
|
435
|
+
| `rb.pub(topic, payload)` | `@plugin.subscribe(topic)` | UI → 백엔드 단방향 |
|
|
436
|
+
| `rb.sub(topic, handler)` | `plugin.publish(topic, payload)` | 백엔드 → UI 스트림 |
|
|
437
|
+
| `rb.action(topic, payload?)` | `@plugin.serve(topic)` | UI 요청, 백엔드 응답 |
|
|
438
|
+
| `rb.serve(topic, handler)` | `plugin.call(topic, payload=...)` | 백엔드 요청, UI 응답 |
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
const rb = useRb();
|
|
442
|
+
|
|
443
|
+
await rb.pub("event", { clicked: true });
|
|
444
|
+
|
|
445
|
+
const sub = await rb.sub<{ ready: boolean }>("state", (s) => setReady(s.ready));
|
|
446
|
+
// 정리: sub.undeclare()
|
|
447
|
+
|
|
448
|
+
const res = await rb.action<{ ok: boolean }, { width: number }>("grip", { width: 20 });
|
|
449
|
+
|
|
450
|
+
await rb.serve("tool/ping", (payload) => ({ pong: payload }));
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
- 토픽은 플러그인 id 범위로 격리됩니다: `"state"` → `{pluginId}/state`. 앞에 `/`를 붙이면 절대 경로입니다 (`"/broadcast/x"`).
|
|
454
|
+
- 네 메서드 모두 payload / 결과 타입을 제네릭으로 받습니다.
|
|
455
|
+
- 구독 해제는 `useEffect` cleanup에서 합니다:
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
let sub: { undeclare(): Promise<void> } | undefined;
|
|
460
|
+
rb.sub("state", setState).then((s) => (sub = s));
|
|
461
|
+
return () => void sub?.undeclare();
|
|
462
|
+
}, [rb]);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### 6.3 호스트 다이얼로그와 토스트
|
|
466
|
+
|
|
467
|
+
| API | 반환 |
|
|
468
|
+
|---|---|
|
|
469
|
+
| `rb.toast({ title?, message, type?, actionLabel?, handleActionClick?, closeOnActionClick?, duration?, onClose? })` | 없음 (`message`: 문자열 또는 ReactElement, `type`: `info`, `danger`, `warning`, `caution`, `success`) |
|
|
470
|
+
| `rb.alert({ title?, message, buttonText? })` | 닫힐 때 resolve |
|
|
471
|
+
| `rb.confirm({ title?, message, buttonText?: { yes, no } })` | `true` / `false` |
|
|
472
|
+
| `rb.prompt({ title?, message, value?, placeholder?, buttonText?: { yes, no } })` | 입력 문자열, 취소 시 `undefined` |
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
if (await rb.confirm({ title: "실행", message: "백엔드 동작을 시작할까요?" })) {
|
|
476
|
+
await rb.action("run");
|
|
477
|
+
rb.toast({ message: "시작됨", type: "success" });
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### 6.4 Program 페이지 (`rb.program`)
|
|
482
|
+
|
|
483
|
+
| API | 의미 |
|
|
484
|
+
|---|---|
|
|
485
|
+
| `rb.program.getSteps(scope?)` | Program 페이지에 열린 프로그램의 메인 스텝 트리 |
|
|
486
|
+
| `rb.program.setSteps(steps, scope?)` | 메인 스텝 트리 교체. 저장되지 않은 편집으로 들어가고 사용자가 저장 |
|
|
487
|
+
| `rb.program.getCatalog(scope?)` | 로드된 로봇의 명령 카탈로그 (이름, 기본 인자, 중첩 규칙) |
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
type PluginProgramStep = {
|
|
491
|
+
command: string; // "MoveJ", "Point", "Wait", "Repeat", "Condition", ...
|
|
492
|
+
args?: Record<string, unknown>;
|
|
493
|
+
summary?: string;
|
|
494
|
+
fields?: Record<string, unknown>; // args 외 스텝 필드, 예: SubTask의 subTaskType
|
|
495
|
+
children?: PluginProgramStep[];
|
|
496
|
+
};
|
|
497
|
+
type PluginProgramScope = { taskId?: string; robotModel?: string };
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
`scope`가 없으면 첫 번째 main task를 씁니다. `program.monitor` 탭에서는 `rb.data`가 `{ point: "program.monitor", taskId, robotModel }`이므로 이를 `scope`로 넘겨야 다중 컴포넌트 로봇에서 맞는 task로 갑니다.
|
|
501
|
+
|
|
502
|
+
### 6.5 스텝 폼 저장/복원 (`program.panel`)
|
|
503
|
+
|
|
504
|
+
| 훅 | 의미 |
|
|
505
|
+
|---|---|
|
|
506
|
+
| `useRbFormData<T>()` | 이 스텝에 저장된 값. 새 스텝이면 `undefined` |
|
|
507
|
+
| `useRbSave(fn)` | 호스트가 저장할 때 호출할 getter 등록. 항상 최신 상태를 봄. 반환값은 JSON 직렬화 가능해야 함 |
|
|
508
|
+
|
|
509
|
+
```tsx
|
|
510
|
+
import { useState } from "react";
|
|
511
|
+
import { RBSlider, useRbFormData, useRbSave } from "@rainbow-robotics/plugin-ui/react";
|
|
512
|
+
|
|
513
|
+
type Args = { width: number };
|
|
514
|
+
|
|
515
|
+
export default function App() {
|
|
516
|
+
const saved = useRbFormData<Args>();
|
|
517
|
+
const [args, setArgs] = useState<Args>({ width: 20, ...saved });
|
|
518
|
+
useRbSave(() => ({ args, summary: `${args.width}mm` }));
|
|
519
|
+
return (
|
|
520
|
+
<RBSlider
|
|
521
|
+
value={[args.width]}
|
|
522
|
+
min={0}
|
|
523
|
+
max={100}
|
|
524
|
+
onValueChange={(values: number[]) => setArgs({ width: values[0] ?? 0 })}
|
|
525
|
+
/>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
저장된 `args`는 백엔드 `on_execute(args)`로 전달됩니다 (7.3 참고).
|
|
531
|
+
|
|
532
|
+
### 6.6 브라우저 저장소 (`rb.storage`, `useRbStorage`, `createRbStore`)
|
|
533
|
+
|
|
534
|
+
플러그인의 브라우저 쪽 데이터는 여기에만 저장합니다. 모든 데이터가 `muscat-plugin:<pluginId>`로 구분되고, 플러그인을 삭제하면 호스트가 지웁니다. 다른 방법(`localStorage` 직접 사용, 자체 IndexedDB)으로 저장한 데이터는 정리되지 않습니다.
|
|
535
|
+
|
|
536
|
+
| 저장소 | 용도 |
|
|
537
|
+
|---|---|
|
|
538
|
+
| `rb.storage` (= `rb.storage.indexedDB`) | 기본값. 구조화된 값(객체, 배열, `Date`, `Blob` 등) |
|
|
539
|
+
| `rb.storage.local` | `localStorage`, JSON 값, 새로고침해도 유지 |
|
|
540
|
+
| `rb.storage.session` | `sessionStorage`, JSON 값, 탭을 닫으면 사라짐 |
|
|
541
|
+
| `rb.storage.cookie` | 값당 약 4 KB, 호스트로 가는 모든 요청에 실려 감 — 아주 작은 값만 |
|
|
542
|
+
|
|
543
|
+
모든 저장소의 API는 같습니다:
|
|
544
|
+
|
|
545
|
+
| 메서드 | 의미 |
|
|
546
|
+
|---|---|
|
|
547
|
+
| `get<T>(key)` | 값, 없으면 `undefined` |
|
|
548
|
+
| `set(key, value)` | 저장 |
|
|
549
|
+
| `delete(key)` | 삭제 |
|
|
550
|
+
| `keys()` | 이 플러그인의 키 목록 |
|
|
551
|
+
| `subscribe(listener, key?)` | 이 API로 바뀐 모든 변경에 `({ kind, key, value }) => void` 호출 — 현재 화면, 같은 플러그인의 다른 화면(툴바 패널, Program 탭 등), 다른 브라우저 탭 포함. 삭제 후에는 `value`가 `undefined`. 구독 해제 함수 반환 |
|
|
552
|
+
| `getItem` / `setItem` / `removeItem` | zustand `StateStorage` (문자열 값) |
|
|
553
|
+
|
|
554
|
+
```tsx
|
|
555
|
+
const rb = useRb();
|
|
556
|
+
await rb.storage.set("lastRun", { at: new Date(), ok: true });
|
|
557
|
+
const last = await rb.storage.get<{ at: Date; ok: boolean }>("lastRun");
|
|
558
|
+
await rb.storage.local.set("theme", "dark");
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
#### `useRbStorage` — 저장된 값 하나를 React state로
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
import { useRbStorage } from "@rainbow-robotics/plugin-ui/react";
|
|
565
|
+
|
|
566
|
+
type Draft = { name: string };
|
|
567
|
+
|
|
568
|
+
function Editor() {
|
|
569
|
+
const [draft, setDraft] = useRbStorage<Draft>("draft", { kind: "local", initial: { name: "" } });
|
|
570
|
+
// setDraft(next)는 저장, setDraft(undefined)는 삭제
|
|
571
|
+
return <RBTextInput value={draft?.name ?? ""} handleChange={(name: string) => void setDraft({ name })} />;
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
- `useRbStorage<T>(key, { kind?, initial? })` → `[value, setValue]`. `kind` 기본값은 `"indexeddb"`.
|
|
576
|
+
- 값이 어디서 바뀌든(다른 화면, 다른 탭) 다시 렌더링됩니다.
|
|
577
|
+
- 저장된 값을 불러오기 전까지 `value`는 `initial`입니다.
|
|
578
|
+
|
|
579
|
+
#### `createRbStore` — 저장되는 zustand 스타일 store
|
|
580
|
+
|
|
581
|
+
모듈 최상위에서 한 번 만들고 플러그인의 어느 화면에서든 씁니다:
|
|
582
|
+
|
|
583
|
+
```tsx
|
|
584
|
+
import { createRbStore } from "@rainbow-robotics/plugin-ui/react";
|
|
585
|
+
|
|
586
|
+
type Settings = {
|
|
587
|
+
speed: number;
|
|
588
|
+
points: string[];
|
|
589
|
+
setSpeed: (v: number) => void;
|
|
590
|
+
addPoint: (p: string) => void;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
export const useSettings = createRbStore<Settings>(
|
|
594
|
+
(set, get) => ({
|
|
595
|
+
speed: 50,
|
|
596
|
+
points: [],
|
|
597
|
+
setSpeed: (speed) => set({ speed }),
|
|
598
|
+
addPoint: (p) => set((s) => ({ points: [...s.points, p] })),
|
|
599
|
+
}),
|
|
600
|
+
{ name: "settings", storage: "local" },
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
function SpeedPanel() {
|
|
604
|
+
const speed = useSettings((s) => s.speed); // speed가 바뀔 때만 다시 렌더링
|
|
605
|
+
const setSpeed = useSettings((s) => s.setSpeed);
|
|
606
|
+
return <RBSlider value={[speed]} min={0} max={100} onValueChange={(v: number[]) => setSpeed(v[0] ?? 0)} />;
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
| 멤버 | 의미 |
|
|
611
|
+
|---|---|
|
|
612
|
+
| `createRbStore<T>(init, { name, storage? })` | `init(set, get)`이 초기 상태 + 액션을 반환. `name`은 저장 키, `storage`는 저장소 종류 (기본 `"indexeddb"`) |
|
|
613
|
+
| `useStore()` / `useStore(selector)` | 전체 상태 / 일부 (그 부분이 바뀔 때만 다시 렌더링) |
|
|
614
|
+
| `useStore.getState()` | React 밖에서 현재 상태 읽기 |
|
|
615
|
+
| `useStore.setState(partial \| (s) => partial)` | React 밖에서 변경 병합 |
|
|
616
|
+
| `useStore.subscribe((next, prev) => …)` | React 밖에서 변경 구독. 해제 함수 반환 |
|
|
617
|
+
|
|
618
|
+
- 상태는 `set` / `setState`로만 바뀝니다. 바뀔 때마다 저장되고, 이 store를 쓰는 모든 화면과 탭이 다시 렌더링됩니다.
|
|
619
|
+
- 함수(액션)는 저장되지 않고 데이터 필드만 저장됩니다.
|
|
620
|
+
- 저장된 데이터는 이 store를 쓰는 첫 컴포넌트가 마운트될 때 불러옵니다 (그때서야 플러그인 id를 알 수 있음). 그 전에는 `getState()`가 초기값을 반환합니다.
|
|
621
|
+
- 변경은 얕게 병합됩니다: 배열/객체는 직접 수정하지 말고 새로 만들어 넣으세요 (`[...s.points, p]`).
|
|
622
|
+
|
|
623
|
+
#### zustand `persist`를 직접 쓰기
|
|
624
|
+
|
|
625
|
+
모든 저장소는 zustand `StateStorage`입니다:
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
import { persist, createJSONStorage } from "zustand/middleware";
|
|
629
|
+
persist(fn, { name: "settings", storage: createJSONStorage(() => rb.storage.local) });
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
이 경우 `zustand`를 플러그인 의존성으로 설치해야 합니다. `createRbStore`는 추가 설치가 필요 없습니다.
|
|
633
|
+
|
|
634
|
+
### 6.7 기타
|
|
635
|
+
|
|
636
|
+
| API | 의미 |
|
|
637
|
+
|---|---|
|
|
638
|
+
| `rb.data` | 호스트가 마운트 시 넘기는 데이터 (`program.monitor` scope, 저장된 폼 값) |
|
|
639
|
+
| `rb.rosPeers.get()` | 이 플러그인의 ROS 2 노드에 접근을 허용한 PC 목록 |
|
|
640
|
+
| `rb.rosPeers.set(ips)` | 목록 저장 후 백엔드 재시작. 잘못된 IP면 reject |
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 7. Python 플러그인 SDK
|
|
645
|
+
|
|
646
|
+
Python 백엔드(`runtime.type: "python"`, 엔트리 `py/main.py`)는 `rainbow-rb-sdk`를 씁니다. 버전은 `requirements.txt`에 고정하고 `muscat-pk sdk update`로 바꿉니다. 실행 시 SDK는 로봇의 플러그인 컨테이너가 제공합니다.
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
from rainbow.rb_sdk import RBPluginCall, PluginProgramContext, RBManipulateSDK, RBAmrSDK
|
|
650
|
+
from rainbow.rb_sdk.plugin import PluginRouteError
|
|
651
|
+
from rainbow.rb_sdk.rb_sdk_cpp import ConnectOptions
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
어떤 API를 쓸지는 contribution point로 정해집니다:
|
|
655
|
+
|
|
656
|
+
| contribution point | 백엔드 |
|
|
657
|
+
|---|---|
|
|
658
|
+
| `toolbar` / `lnb` / `move.panel` / `program.monitor` | `RBPluginCall` + 로봇 SDK(`RBManipulateSDK`, `RBAmrSDK`) 직접 호출. 정지/일시정지/변수 컨텍스트 없음 |
|
|
659
|
+
| `program.panel` | `PluginProgramContext` — 프로그램 스텝으로 실행되며 정지/일시정지·프로그램 변수와 연동 |
|
|
660
|
+
|
|
661
|
+
### 7.1 `RBPluginCall` — UI ↔ 백엔드
|
|
662
|
+
|
|
663
|
+
내부 전용입니다 (UI ↔ 백엔드, 플러그인 ↔ 플러그인). Muscat 밖에서는 접근할 수 없습니다. 플러그인 id는 환경 변수 `RB_PLUGIN_ID`에서 가져옵니다.
|
|
664
|
+
|
|
665
|
+
```python
|
|
666
|
+
plugin = RBPluginCall()
|
|
667
|
+
|
|
668
|
+
@plugin.serve("grip") # UI: await rb.action("grip", { width })
|
|
669
|
+
def grip(payload=None):
|
|
670
|
+
width = (payload or {}).get("width", 20)
|
|
671
|
+
plugin.publish("state", {"last_grip_width": width})
|
|
672
|
+
return {"ok": True, "width": width}
|
|
673
|
+
|
|
674
|
+
@plugin.subscribe("event") # UI: rb.pub("event", ...)
|
|
675
|
+
def on_event(payload=None):
|
|
676
|
+
print(payload)
|
|
677
|
+
|
|
678
|
+
plugin.publish("state", {"ready": True}) # UI: rb.sub("state", ...)
|
|
679
|
+
res = plugin.call("tool/ping", payload={"hi": 1}, timeout=3.0) # UI: rb.serve("tool/ping", ...)
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
| 멤버 | 의미 |
|
|
683
|
+
|---|---|
|
|
684
|
+
| `serve(keyexpr, handler=None)` | 요청/응답 핸들러. 데코레이터 또는 직접 호출. 반환값이 응답 |
|
|
685
|
+
| `subscribe(topic, callback=None)` | publish 수신 |
|
|
686
|
+
| `publish(topic, payload=None)` | 구독자에게 발행 |
|
|
687
|
+
| `call(keyexpr, *, payload, timeout=3.0)` | 상대편에 요청하고 응답 대기 |
|
|
688
|
+
| `unserve(keyexpr=None)` | serve 핸들러 하나 / 전체 해제 |
|
|
689
|
+
| `unsubscribe(topic=None, callback=None)` | 구독 해제 |
|
|
690
|
+
| `plugin_id` | 사용 중인 플러그인 id (`muscat-pk dev`에서는 dev id) |
|
|
691
|
+
| `connected` | 연결 여부 |
|
|
692
|
+
| `data_dir` | 영구 데이터 디렉터리 (접근 시 생성). 업데이트·재시작에도 유지되고 플러그인 삭제 시 제거. 설치 디렉터리(cwd)는 업데이트마다 교체되므로 상태·캐시·설정은 여기에 저장 |
|
|
693
|
+
| `router` | 외부 엔드포인트 (7.2) |
|
|
694
|
+
|
|
695
|
+
payload는 JSON 직렬화 가능해야 합니다. 핸들러는 sync/async 모두 됩니다.
|
|
696
|
+
|
|
697
|
+
### 7.2 `plugin.router` — 외부 엔드포인트
|
|
698
|
+
|
|
699
|
+
`common/plugin/{plugin_id}/{path}`를 REST, Socket.IO, TCP, MQTT, WebRTC로 노출합니다. 실행 중인 Muscat(`common`)이 재시작 없이 반영하고, 플러그인이 멈추면 사라집니다. 로봇에 `rainbow-rb-sdk` 0.0.9.dev5 이상이 필요합니다 (그보다 낮으면 `AttributeError`).
|
|
700
|
+
|
|
701
|
+
```python
|
|
702
|
+
@plugin.router.on("/hello") # 모든 transport
|
|
703
|
+
def hello(payload, meta):
|
|
704
|
+
return {"msg": f"hi {payload.get('name')}", "via": meta["transport"]}
|
|
705
|
+
|
|
706
|
+
@plugin.router.on("/admin", explicit_routes=["api"]) # REST만
|
|
707
|
+
def admin(payload, meta):
|
|
708
|
+
if not (meta.get("principal") or {}).get("is_admin"):
|
|
709
|
+
raise PluginRouteError(403, "admin only")
|
|
710
|
+
return {"ok": True}
|
|
711
|
+
|
|
712
|
+
@plugin.router.subscribe("/cmd", explicit_events=["tcp"])
|
|
713
|
+
def on_cmd(payload):
|
|
714
|
+
...
|
|
715
|
+
|
|
716
|
+
plugin.router.emit("/state", {"busy": True})
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
| API | 의미 | 외부 쪽 |
|
|
720
|
+
|---|---|---|
|
|
721
|
+
| `router.on(path, explicit_routes=None)` | 요청/응답 | `common/plugin/{id}/{path}` 호출 (REST는 `POST`) |
|
|
722
|
+
| `router.subscribe(topic, explicit_events=None)` | 외부 publish 수신 | `common/plugin/{id}/{topic}`에 publish |
|
|
723
|
+
| `router.emit(topic, payload=None)` | 외부 구독자에게 push | `common/plugin/{id}/{topic}` 구독 |
|
|
724
|
+
|
|
725
|
+
핸들러 규칙:
|
|
726
|
+
|
|
727
|
+
- 시그니처 `(payload, meta)`, `(payload)`, `()` — 선언한 인자만 주입됩니다. sync/async 모두 가능.
|
|
728
|
+
- `payload`: 호출자가 보낸 값 (REST는 JSON body + query param 병합).
|
|
729
|
+
- `meta`: `{"transport": "api"|"socket"|"tcp"|"mqtt"|"webrtc", "principal": dict | None}`. 로그인 정책이 켜져 있으면 `principal`에 토큰 주체가 들어 있습니다. 권한 체크는 플러그인이 직접 합니다.
|
|
730
|
+
- 반환값이 응답 body (JSON). `None`이면 빈 응답.
|
|
731
|
+
- `raise PluginRouteError(code, message)`로 상태 코드 지정. 다른 예외는 500 + `str(exc)`.
|
|
732
|
+
- `subscribe` 핸들러는 `(payload)`만 받고 반환값이 없습니다.
|
|
733
|
+
|
|
734
|
+
규칙:
|
|
735
|
+
|
|
736
|
+
- 경로만 적습니다. `plugin_id`는 SDK가 붙입니다. 여러 단계 경로도 됩니다 (`/tool/echo`). 같은 경로를 두 번 선언하면 `ValueError`.
|
|
737
|
+
- `explicit_routes` / `explicit_events`: `None`이면 모든 transport. 이름은 `api | socket | tcp | mqtt | webrtc`이고 `subscribe`는 `tcp | mqtt | webrtc`만. 모르는 이름은 `ValueError`. 허용 안 된 transport로 오면 404.
|
|
738
|
+
- `on`과 `subscribe`는 같은 이름을 써도 됩니다.
|
|
739
|
+
|
|
740
|
+
외부에서 호출하기:
|
|
741
|
+
|
|
742
|
+
| Transport | 방법 |
|
|
743
|
+
|---|---|
|
|
744
|
+
| REST | `POST /common/plugin/{id}/{path}`, JSON body |
|
|
745
|
+
| Socket.IO | `common/plugin/{id}/{path}` 이벤트 emit, 응답은 ack로 |
|
|
746
|
+
| TCP | 토픽 `common/plugin/{id}/{path}` 요청 / publish |
|
|
747
|
+
| MQTT | 요청 `common/plugin/{id}/{path}/__req__`, publish `common/plugin/{id}/{topic}` |
|
|
748
|
+
| WebRTC | data channel 토픽 `common/plugin/{id}/{path}` |
|
|
749
|
+
|
|
750
|
+
```bash
|
|
751
|
+
curl -s -X POST http://<robot>/common/plugin/com.example.my-plugin/hello \
|
|
752
|
+
-H 'Content-Type: application/json' -d '{"name":"kim"}'
|
|
753
|
+
# muscat-pk dev 중 (dev id는 시작할 때 출력됨)
|
|
754
|
+
curl -s -X POST http://localhost:3000/common/plugin/<dev-id>/hello -H 'Content-Type: application/json' -d '{}'
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
| 상황 | 상태 코드 |
|
|
758
|
+
|---|---|
|
|
759
|
+
| 경로 없음 / 플러그인 꺼짐 / 허용 안 된 transport | 404 |
|
|
760
|
+
| 10초 안에 응답 없음 | 504 |
|
|
761
|
+
| `common` ↔ gateway 연결 안 됨 | 503 |
|
|
762
|
+
| 핸들러 예외 | 500 (`PluginRouteError`면 그 코드) |
|
|
763
|
+
|
|
764
|
+
인증: 로그인 정책이 켜져 있으면 토큰이 필요합니다 (역할 무관). REST는 다른 Muscat API와 같은 Bearer 토큰을 씁니다. 꺼져 있으면 토큰 없이 됩니다.
|
|
765
|
+
|
|
766
|
+
알려진 제약:
|
|
767
|
+
|
|
768
|
+
- 경로 이름이 `install`이면 REST로 호출할 수 없습니다 (레지스트리 설치 라우트가 먼저 잡음).
|
|
769
|
+
- MQTT 브로커는 보낸 쪽에도 메시지를 돌려줍니다. 같은 토픽에 `emit`과 `subscribe`를 함께 쓰면 자기 push를 `mqtt`로 받습니다. `explicit_events`에서 `mqtt`를 빼거나 토픽을 나누세요.
|
|
770
|
+
- 플러그인 재시작 직후 외부 호출은 gateway가 이전 소켓을 정리할 때까지 최대 1분 정도 504, 그 뒤 404가 날 수 있습니다.
|
|
771
|
+
|
|
772
|
+
### 7.3 `PluginProgramContext` — 프로그램 명령
|
|
773
|
+
|
|
774
|
+
`program.panel`에서만 씁니다. 프로그램이 이 스텝에 도달하면 실행기가 `commandInfo.funcName`을 `plugin_id` + 스텝 인자로 호출하고, SDK가 `on_execute`를 부릅니다.
|
|
775
|
+
|
|
776
|
+
```python
|
|
777
|
+
import asyncio
|
|
778
|
+
from rainbow.rb_sdk import PluginProgramContext
|
|
779
|
+
|
|
780
|
+
ctx = PluginProgramContext(plugin_id="com.example.my-plugin")
|
|
781
|
+
|
|
782
|
+
@ctx.on_execute
|
|
783
|
+
async def handle(args: dict) -> None:
|
|
784
|
+
count = await ctx.get_variable("counter")
|
|
785
|
+
await ctx.set_variable("counter", (count or 0) + 1)
|
|
786
|
+
await ctx.set_variable("RBP_my_plugin_result", 42) # 새 변수 → RBP_ 필수
|
|
787
|
+
|
|
788
|
+
for _ in range(100):
|
|
789
|
+
await ctx.check_stop() # 정지 → CancelledError, 일시정지 → 재개까지 대기
|
|
790
|
+
await asyncio.sleep(0.5)
|
|
791
|
+
|
|
792
|
+
await ctx.done() # 반드시 호출. 안 하면 스텝이 끝나지 않음
|
|
793
|
+
|
|
794
|
+
@ctx.on_executor_state_change
|
|
795
|
+
async def on_change(robot_model: str, state: str) -> None:
|
|
796
|
+
if state == "paused":
|
|
797
|
+
print(f"{robot_model} paused")
|
|
798
|
+
|
|
799
|
+
async def background() -> None:
|
|
800
|
+
while True:
|
|
801
|
+
await asyncio.sleep(1.0)
|
|
802
|
+
await ctx.set_variable("RBP_my_plugin_heartbeat", 1) # 프로그램이 없으면 무시됨
|
|
803
|
+
|
|
804
|
+
async def main() -> None:
|
|
805
|
+
asyncio.create_task(background())
|
|
806
|
+
await ctx.start() # 상주
|
|
807
|
+
|
|
808
|
+
asyncio.run(main())
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
| 멤버 | 의미 |
|
|
812
|
+
|---|---|
|
|
813
|
+
| `on_execute(fn)` | 데코레이터. 이 스텝이 실행될 때마다 스텝 인자로 1회 호출 |
|
|
814
|
+
| `done()` | 스텝 완료. 반드시 호출 |
|
|
815
|
+
| `check_stop(robot_model=None)` | 정지 → `CancelledError`, 일시정지 → 대기. 루프마다 호출 |
|
|
816
|
+
| `get_variable(name, robot_model=None, plugin_id=None)` | 읽기. 프로그램이 없으면 `None` |
|
|
817
|
+
| `set_variable(name, value)` | 실행 중인 모든 main task에 쓰기. 프로그램이 없으면 무시 |
|
|
818
|
+
| `executor_state(robot_model=None)` | 모델 하나의 상태 문자열, 또는 전체 `{model: state}` |
|
|
819
|
+
| `is_paused(robot_model=None)` / `is_stopped(robot_model=None)` | 상태 확인 |
|
|
820
|
+
| `on_executor_state_change(fn)` | 데코레이터. 상태가 바뀔 때마다 `(robot_model, state)`로 1회 |
|
|
821
|
+
| `start()` / `stop()` / `pause()` | 수명 주기 |
|
|
822
|
+
| `in_execute`, `exec_robot_model`, `plugin_id`, `started`, `control_topic` | 속성 |
|
|
823
|
+
|
|
824
|
+
#### 변수
|
|
825
|
+
|
|
826
|
+
- 이미 있는 변수: 이름 그대로 씁니다. 프로그램과 값을 주고받는 것이 원래 용도입니다.
|
|
827
|
+
- 새 변수: 스텝 안이든 밖이든 `RBP_`로 시작해야 합니다 (아니면 `RuntimeError`). 충돌을 피하려면 `RBP_{plugin}_{name}` 형식을 권장합니다. `RBP_`만으로는 두 플러그인이 서로 덮어쓰는 것을 막지 못합니다.
|
|
828
|
+
- Program 페이지에서 고를 수 있게 하려면 `manifest.json` → `variables`에 선언합니다 (5.5).
|
|
829
|
+
|
|
830
|
+
`set_variable`은 실행 중인 모든 main task에 적용됩니다:
|
|
831
|
+
|
|
832
|
+
| 시점 | 자기 프로그램 | 다른 로봇의 main task |
|
|
833
|
+
|---|---|---|
|
|
834
|
+
| 자기 스텝 실행 중 | 즉시 | 각자 다음 스텝 경계에서 |
|
|
835
|
+
| 스텝 밖 | 다음 스텝 경계에서 | 동일 |
|
|
836
|
+
|
|
837
|
+
`get_variable` 옵션:
|
|
838
|
+
|
|
839
|
+
```python
|
|
840
|
+
await ctx.get_variable("counter") # main task 전체에서 처음 찾은 값
|
|
841
|
+
await ctx.get_variable("counter", robot_model="C500920") # 특정 로봇
|
|
842
|
+
await ctx.get_variable("RBP_TEST", plugin_id="A.plugin") # 그 플러그인이 마지막으로 쓴 값
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
변수마다 플러그인별 기록(`meta.writers`)이 남아서 이름이 겹쳐도 값을 되찾을 수 있습니다: SDK에서는 `get_variable(..., plugin_id=...)`, 프로그램 식에서는 `get_var("A.plugin", "RBP_TEST")`. 드물게 worker의 변수 flush와 경합해 쓰기가 유실될 수 있으니 중요한 값은 다시 확인하세요.
|
|
846
|
+
|
|
847
|
+
#### 실행기 상태
|
|
848
|
+
|
|
849
|
+
상태: `idle | running | paused | waiting | stopped | post_start | completed | error`. 실행 중인 task가 없는 모델은 `None` / dict에 없음.
|
|
850
|
+
|
|
851
|
+
- `muscat/executor/state`가 0.8초마다 발행되므로 최대 약 1초 지연됩니다.
|
|
852
|
+
- `IS_DEV=true`이면 발행이 없어서 항상 `None`입니다.
|
|
853
|
+
- `on_executor_state_change`는 모델별로 상태가 바뀔 때 한 번만 호출되고, 핸들러 예외는 로그만 남깁니다.
|
|
854
|
+
|
|
855
|
+
`check_stop` / `is_paused` / `is_stopped`의 대상 범위:
|
|
856
|
+
|
|
857
|
+
1. `robot_model`을 주면 → 그 모델
|
|
858
|
+
2. `on_execute` 안에서 생략 → 이 스텝을 실행 중인 모델
|
|
859
|
+
3. 그 밖에서 생략 → 모든 모델
|
|
860
|
+
|
|
861
|
+
### 7.4 백엔드에서 로봇 SDK 쓰기
|
|
862
|
+
|
|
863
|
+
```python
|
|
864
|
+
manipulate = RBManipulateSDK()
|
|
865
|
+
manipulate.move.call_move_j(
|
|
866
|
+
robot_model="C500920",
|
|
867
|
+
target={"tar_values": [0, 0, 0, 0, 0, 0, 0], "tar_frame": -1, "tar_unit": 0},
|
|
868
|
+
)
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
로봇을 움직이기 전에 안전 확인을 직접 넣으세요. 정확한 시그니처는 `muscat-pk mcp`(`search_rb_sdk`, `get_rb_sdk_api`)로 확인합니다.
|
|
872
|
+
|
|
873
|
+
### 7.5 원격 개발 (노트북 → 로봇)
|
|
874
|
+
|
|
875
|
+
```bash
|
|
876
|
+
export MUSCAT_ROBOT_HOST=192.168.1.100
|
|
877
|
+
export MUSCAT_ROBOT_TOKEN=<service token> # 로봇 로그인 정책이 켜져 있을 때만
|
|
878
|
+
muscat-pk dev:back
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
```python
|
|
882
|
+
async def main():
|
|
883
|
+
host = os.getenv("MUSCAT_ROBOT_HOST")
|
|
884
|
+
if host:
|
|
885
|
+
opts = ConnectOptions()
|
|
886
|
+
opts.host = host
|
|
887
|
+
opts.token = os.getenv("MUSCAT_ROBOT_TOKEN", "")
|
|
888
|
+
manipulate.connect(opts)
|
|
889
|
+
register_handlers() # connect() 이후에 — connect()가 세션을 다시 열어서 그 전에 등록한 핸들러가 사라짐
|
|
890
|
+
...
|
|
891
|
+
if host:
|
|
892
|
+
await manipulate.disconnect()
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
스캐폴드된 `py/main.py`가 이미 이 구조입니다.
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## 8. 다른 백엔드 런타임 (C++ / ROS 2)
|
|
900
|
+
|
|
901
|
+
`none`이 아닌 런타임은 `dev:back`, `dev`, `build` 때 모두 Docker에서 실행됩니다.
|
|
902
|
+
|
|
903
|
+
### 8.1 C++ (`cpp`)
|
|
904
|
+
|
|
905
|
+
- 소스는 `cpp/`. Docker 안에서 C++ SDK(apt `rainbow-rb-sdk-dev`)로 CMake 빌드합니다.
|
|
906
|
+
- CMake: `find_package(rb_sdk <ver> EXACT REQUIRED)` + `target_link_libraries(<target> rb_sdk::rb_sdk)` (C++20).
|
|
907
|
+
- 헤더: `<rainbow/rb_sdk/manipulate/rb_manipulate_sdk.hpp>`, `<rainbow/rb_sdk/plugin/rb_plugin.hpp>` (`rb_plugin::RBPluginCall`), `<rainbow/rb_sdk/program/plugin_program_context.hpp>` (`rb_plugin::PluginProgramContext`).
|
|
908
|
+
- 동적 링크. `build`가 `librb_sdk.so`를 `dist/<arch>/lib/`에 복사합니다.
|
|
909
|
+
|
|
910
|
+
컨테이너 밖(Ubuntu 24.04)에서 빌드할 때:
|
|
911
|
+
|
|
912
|
+
```bash
|
|
913
|
+
curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/rb-sdk-apt.gpg
|
|
914
|
+
echo 'deb [signed-by=/usr/share/keyrings/rb-sdk-apt.gpg] https://rainbowrobotics.github.io/rb-sdk stable main' | sudo tee /etc/apt/sources.list.d/rb-sdk.list
|
|
915
|
+
sudo apt update && sudo apt install rainbow-rb-sdk-dev
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
### 8.2 ROS 2 (`ros2` + `rclpy` / `rclcpp`)
|
|
919
|
+
|
|
920
|
+
- 소스는 `ros2_ws/src/<package>/`. `runtime.package` = colcon 패키지, `runtime.launch` = 노드 실행 파일.
|
|
921
|
+
- 상대 토픽 이름은 플러그인 namespace 아래에 놓입니다: id의 `.` → `/`, `-` → `_` (`com.example.my-plugin` → `/com/example/my_plugin`). 다른 노드와 공유하는 토픽은 절대 이름(`/cmd_vel`)을 씁니다.
|
|
922
|
+
- rclpy 노드에서도 `RBPluginCall`을 쓸 수 있습니다.
|
|
923
|
+
- ROS discovery는 로봇 안으로 제한됩니다 (`ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`). 외부 PC는 UI에서 `rb.rosPeers.set([...])`으로 허용하면 Muscat이 `ROS_STATIC_PEERS`를 넣고 백엔드를 재시작합니다. dev에서는 목록이 `.muscat-dev/ros-peers.json`에 저장됩니다.
|
|
924
|
+
|
|
925
|
+
---
|
|
926
|
+
|
|
927
|
+
## 9. 개발 환경
|
|
928
|
+
|
|
929
|
+
### 9.1 로컬 Muscat 런타임
|
|
930
|
+
|
|
931
|
+
로봇 IP 없이 `dev` / `dev:ui`를 돌리면 `localhost:3000`에 Muscat이 필요합니다:
|
|
932
|
+
|
|
933
|
+
- 이미 `:10000`을 쓰는 컨테이너(`muscat`, `muscat-dev` 등)가 있으면 재사용하고,
|
|
934
|
+
- 없으면 `muscat-pk-muscat`을 띄웁니다 — `common`만 돌리는 S3 런타임 이미지이며 `~/.muscat-pk/muscat/`에 캐시됩니다. 실행 사이에도 계속 떠 있습니다.
|
|
935
|
+
|
|
936
|
+
```bash
|
|
937
|
+
docker rm -f muscat-pk-muscat # 중지
|
|
938
|
+
MUSCAT_PK_NO_LOCAL_MUSCAT=1 muscat-pk dev # 띄우지 않기
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
### 9.2 로봇 PC에서 직접 개발
|
|
942
|
+
|
|
943
|
+
로봇 PC에서는 Muscat이 이미 `10000`에서 gateway를 열고 있으므로 CLI가 그것을 쓰고 로컬 Muscat을 띄우지 않습니다.
|
|
944
|
+
|
|
945
|
+
### 9.3 여러 명이 한 로봇에서 개발
|
|
946
|
+
|
|
947
|
+
원격 로봇에 붙으면 `dev` / `dev:back`이 플러그인을 `<manifest id>.dev-<user>-<random>`으로 실행합니다 (random 부분은 `~/.muscat-pk/dev-id`에 보관). 그래서 두 PC가 서로의 UI 요청에 응답하지 않습니다. UI, 백엔드, ROS namespace, 외부 라우트가 모두 dev id를 씁니다. `--keep-id`는 manifest id를 그대로 씁니다 (프로그램 `execute`나 실제 id를 쓰는 외부 클라이언트 테스트용). 이때는 한 PC만 붙어야 합니다.
|
|
948
|
+
|
|
949
|
+
### 9.4 로컬 설치 vs 전역 설치
|
|
950
|
+
|
|
951
|
+
프로젝트 `node_modules`에 `muscat-plugin-kit`이 있으면 어떤 `muscat-pk`를 실행해도 그 버전으로 넘깁니다. 그래서 CLI, `vite` 프리셋, `preview-host`가 항상 프로젝트가 고정한 버전입니다. `muscat-pk version`은 실제로 실행된 쪽의 버전을 출력합니다.
|
|
952
|
+
|
|
953
|
+
### 9.5 오프라인 / 폐쇄망
|
|
954
|
+
|
|
955
|
+
Docker 이미지는 없을 때만 빌드합니다 (재빌드는 캐시가 있어도 레지스트리에 접속함).
|
|
956
|
+
|
|
957
|
+
```bash
|
|
958
|
+
docker save muscat-pk-python-dev muscat-pk-runtime | gzip > muscat-pk-images.tar.gz # 인터넷 되는 PC
|
|
959
|
+
gunzip -c muscat-pk-images.tar.gz | docker load # 오프라인 PC
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
강제 재빌드: `MUSCAT_PK_REBUILD_IMAGE=1`.
|
|
963
|
+
|
|
964
|
+
### 9.6 IDE 지원 (Python)
|
|
965
|
+
|
|
966
|
+
`muscat-pk new`가 `requirements.txt`를 설치한 `.venv`를 만듭니다. 에디터 린트용일 뿐이고 백엔드는 Docker에서 돌아갑니다. `uv`나 Python 3가 없으면 `Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.`를 출력하고 넘어갑니다. 설치 후 `new`나 `type change`를 다시 실행하세요.
|
|
967
|
+
|
|
968
|
+
### 9.7 JavaScript 의존성
|
|
969
|
+
|
|
970
|
+
`build`는 프로젝트의 Vite 빌드를 그대로 돌리므로 패키지가 설치돼 있어야 합니다. 빌드 전에 `npm install --omit=dev`를 하지 마세요 — `vite`, `typescript`, `tailwindcss`, `sass`, `less`는 devDependencies입니다. ZIP에는 `node_modules`가 들어가지 않습니다.
|
|
971
|
+
|
|
972
|
+
---
|
|
973
|
+
|
|
974
|
+
## 10. 스타일
|
|
975
|
+
|
|
976
|
+
- `muscat()`이 모든 엔트리에 `ui/style.{css,scss,sass,less}` / `ui/styles.*`를, 이름이 같은 엔트리에 `ui/{entry}.*`를 자동 로드하고 CSS를 JS 번들에 넣습니다.
|
|
977
|
+
- 빌드된 셀렉터는 `[data-muscat-plugin-id="..."]` 아래로 스코프되므로 플러그인 CSS가 호스트나 다른 플러그인에 영향을 주지 않습니다.
|
|
978
|
+
- 색상은 hex 대신 디자인 토큰 class(`bg-bg-*`, `text-fg-*`, `typo-*`)를 씁니다.
|
|
979
|
+
|
|
980
|
+
Tailwind (기본, preflight 없음):
|
|
981
|
+
|
|
982
|
+
```css
|
|
983
|
+
@layer theme, utilities;
|
|
984
|
+
@import "tailwindcss/theme.css" layer(theme);
|
|
985
|
+
@import "tailwindcss/utilities.css" layer(utilities);
|
|
986
|
+
@source "./**/*.{ts,tsx}";
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
SCSS / Less: `npm i -D sass` (또는 `less`) 후 `ui/style.scss` (`ui/style.less`) 생성.
|
|
990
|
+
|
|
991
|
+
Emotion `styled`는 설정 없이 됩니다. `css` prop을 쓰려면:
|
|
992
|
+
|
|
993
|
+
```bash
|
|
994
|
+
npm i @emotion/react @emotion/styled && npm i -D @emotion/babel-plugin
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
```ts
|
|
998
|
+
muscat({ react: { jsxImportSource: "@emotion/react", babel: { plugins: ["@emotion/babel-plugin"] } } })
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
그리고 `tsconfig.json`에 `"jsxImportSource": "@emotion/react"`. Emotion `Global`은 스코프되지 않으니 쓰지 마세요.
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## 11. AI 코딩 에이전트 설정 (`muscat-pk mcp`)
|
|
1006
|
+
|
|
1007
|
+
stdio MCP 서버입니다. rb-components(실제 props, Storybook 예시), 디자인 토큰, Python `rainbow-rb-sdk`(실제 시그니처/docstring)를 에이전트에 제공하고, 오래된 프로젝트 업그레이드용 `check_project`도 있습니다.
|
|
1008
|
+
|
|
1009
|
+
Claude Code — 플러그인 루트의 `.mcp.json`:
|
|
1010
|
+
|
|
1011
|
+
```json
|
|
1012
|
+
{ "mcpServers": { "muscat": { "command": "muscat-pk", "args": ["mcp"] } } }
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
Codex — `muscat-pk mcp --install codex`, 또는 `~/.codex/config.toml`:
|
|
1016
|
+
|
|
1017
|
+
```toml
|
|
1018
|
+
[mcp_servers.muscat]
|
|
1019
|
+
command = "muscat-pk"
|
|
1020
|
+
args = ["mcp"]
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
서버는 에이전트가 직접 띄웁니다.
|
|
1024
|
+
|
|
1025
|
+
---
|
|
1026
|
+
|
|
1027
|
+
## 12. 환경 변수
|
|
1028
|
+
|
|
1029
|
+
| 변수 | 효과 |
|
|
1030
|
+
|---|---|
|
|
1031
|
+
| `MUSCAT_ROBOT_HOST` | `dev` / `dev:ui` / `dev:back`의 로봇 IP (프롬프트 생략) |
|
|
1032
|
+
| `MUSCAT_ROBOT_TOKEN` | 로봇 로그인 정책이 켜져 있을 때 service token |
|
|
1033
|
+
| `MUSCAT_PK_NO_LOCAL_MUSCAT=1` | 로컬 Muscat 런타임을 띄우지 않음 |
|
|
1034
|
+
| `MUSCAT_PK_REBUILD_IMAGE=1` | Docker 이미지 재빌드 |
|
|
1035
|
+
| `MUSCAT_PK_RB_SDK_VERSION` | C++ 이미지에 넣을 C++ SDK 버전 (`MUSCAT_PK_REBUILD_IMAGE=1`과 함께) |
|
|
1036
|
+
| `MUSCAT_PK_NO_UPDATE_CHECK=1` | `dev` / `dev:ui`의 업데이트 확인 생략 |
|
|
1037
|
+
| `RB_REGISTRY` | 업데이트 확인용 npm 레지스트리 (기본 `https://registry.npmjs.org`) |
|
|
1038
|
+
| `MUSCAT_PK_TAG` | 설치 스크립트 채널 / 버전 |
|
|
1039
|
+
| `NO_COLOR` / `FORCE_COLOR` | 출력 색상 |
|
|
1040
|
+
|
|
1041
|
+
백엔드 컨테이너 안: `RB_PLUGIN_ID` (플러그인 id / dev id).
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
## 13. 문제 해결
|
|
1046
|
+
|
|
1047
|
+
| 증상 | 해결 |
|
|
1048
|
+
|---|---|
|
|
1049
|
+
| `manifest.json errors: …` | 나열된 필드를 고칩니다 (5장) |
|
|
1050
|
+
| 선언 안 된 포트로 백엔드가 멈춤 | 포트(28600-28999)를 `ports`에 추가 |
|
|
1051
|
+
| UI 프리뷰에 "no default export" | 엔트리가 `export default App`인지, `vite.config.ts`가 `muscat()`을 쓰는지 확인. `muscat-pk mcp`의 `check_project` 실행 |
|
|
1052
|
+
| `vite not found — UI will not load` | 프로젝트에서 `npm install` |
|
|
1053
|
+
| `AttributeError: router` | 로봇 SDK가 0.0.9.dev5보다 낮음 |
|
|
1054
|
+
| 스텝이 끝나지 않음 | `on_execute`에서 `await ctx.done()` 호출 |
|
|
1055
|
+
| 정지를 눌러도 플러그인이 계속 돎 | 루프마다 `await ctx.check_stop()` 호출 |
|
|
1056
|
+
| `set_variable`에서 `RuntimeError` | 새 변수 이름은 `RBP_`로 시작 |
|
|
1057
|
+
| `get_variable`이 `None` | 실행 중인 프로그램이 없음 (실행기 상태는 `IS_DEV=true`일 때도 `None`) |
|
|
1058
|
+
| 재시작 후 외부 호출 504 | gateway가 이전 소켓을 정리할 때까지 최대 1분 대기 |
|
|
1059
|
+
| 다른 버전의 로컬 설치가 실행됨 | 정상 동작 — 프로젝트 로컬 kit이 우선. `muscat-pk update` |
|