@rainbow-robotics/muscat-plugin-kit 1.10.3-dev.0 → 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/manifest.mjs +1 -0
- 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-B1KKcB6K.js → Color-YHDXOIA2-Cp8LHsF-.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-Ccehtd2V.js → Docs-B2ox3o_G.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CHv6SVVZ.js → Docs-By11aG_M.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-2Fjmybwk.js → Docs-Kg1Im4XT.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DLOo5tzV.js → Docs-LeW_BIsw.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-exoI2PyN.js → Docs-UJwiVwO2.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DtdcK7tN.js → Docs-ik2AhpV0.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-DHVj1Lpb.js → DocsRenderer-CFRXHY34-DPFneFUi.js} +1 -1
- package/cli/storybook-dist/assets/{entry-preview-fUNTcv77.js → entry-preview-BTw3Jh8E.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-Ck1s_iK9.js → iframe-BqPQIkPQ.js} +2 -2
- package/cli/storybook-dist/assets/{index-BkRHCy11.js → index-k_VkBflK.js} +3 -3
- package/cli/storybook-dist/assets/{preview-Cf2hNwTD.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 +3 -4
- 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-CDcrEKt5.js +0 -3
package/README.md
CHANGED
|
@@ -1,50 +1,57 @@
|
|
|
1
1
|
# @rainbow-robotics/muscat-plugin-kit
|
|
2
2
|
|
|
3
|
-
CLI for developing Muscat plugins
|
|
3
|
+
CLI for developing Muscat plugins (`muscat-pk`): scaffolding, dev servers (UI + backend), build/pack, and the SDK references a plugin author needs.
|
|
4
|
+
|
|
5
|
+
[한국어 문서 (README.ko.md)](./README.ko.md)
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
1. [Install](#1-install)
|
|
10
|
+
2. [Quick start](#2-quick-start)
|
|
11
|
+
3. [Commands](#3-commands)
|
|
12
|
+
4. [Project layout](#4-project-layout)
|
|
13
|
+
5. [manifest.json](#5-manifestjson)
|
|
14
|
+
6. [Plugin UI SDK (`useRb`)](#6-plugin-ui-sdk-userb)
|
|
15
|
+
7. [Python plugin SDK](#7-python-plugin-sdk)
|
|
16
|
+
8. [Other backend runtimes (C++ / ROS 2)](#8-other-backend-runtimes-c--ros-2)
|
|
17
|
+
9. [Development environment](#9-development-environment)
|
|
18
|
+
10. [Styles](#10-styles)
|
|
19
|
+
11. [AI coding agent setup (`muscat-pk mcp`)](#11-ai-coding-agent-setup-muscat-pk-mcp)
|
|
20
|
+
12. [Environment variables](#12-environment-variables)
|
|
21
|
+
13. [Troubleshooting](#13-troubleshooting)
|
|
4
22
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
### Option 1 — npm (requires Node.js 20+)
|
|
23
|
+
---
|
|
8
24
|
|
|
9
|
-
|
|
10
|
-
# latest stable
|
|
11
|
-
npm install -g @rainbow-robotics/muscat-plugin-kit
|
|
25
|
+
## 1. Install
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
npm install -g @rainbow-robotics/muscat-plugin-kit@dev
|
|
27
|
+
### npm (Node.js 20+)
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
npm install -g @rainbow-robotics/muscat-plugin-kit
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g @rainbow-robotics/muscat-plugin-kit # latest stable
|
|
31
|
+
npm install -g @rainbow-robotics/muscat-plugin-kit@dev # dev channel
|
|
32
|
+
npm install -g @rainbow-robotics/muscat-plugin-kit@1.2.3 # specific version
|
|
18
33
|
```
|
|
19
34
|
|
|
20
|
-
###
|
|
35
|
+
### Install script (no Node.js required)
|
|
21
36
|
|
|
22
37
|
macOS / Linux / WSL:
|
|
23
38
|
|
|
24
39
|
```bash
|
|
25
|
-
# latest stable
|
|
26
40
|
curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | sh
|
|
27
|
-
|
|
28
|
-
# dev channel
|
|
29
41
|
curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | MUSCAT_PK_TAG=dev sh
|
|
30
|
-
|
|
31
|
-
# specific version
|
|
32
42
|
curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | MUSCAT_PK_TAG=1.2.3 sh
|
|
33
43
|
```
|
|
34
44
|
|
|
35
45
|
Windows PowerShell:
|
|
36
46
|
|
|
37
47
|
```powershell
|
|
38
|
-
# latest stable
|
|
39
48
|
irm https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.ps1 | iex
|
|
40
|
-
|
|
41
|
-
# dev channel
|
|
42
49
|
$env:MUSCAT_PK_TAG="dev"; irm https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.ps1 | iex
|
|
43
50
|
```
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
The script uses npm when Node.js 20+ is present, otherwise it downloads a self-contained binary.
|
|
46
53
|
|
|
47
|
-
###
|
|
54
|
+
### Direct binary download
|
|
48
55
|
|
|
49
56
|
```
|
|
50
57
|
https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/v{version}/{binary}
|
|
@@ -59,129 +66,317 @@ https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/dev/{bi
|
|
|
59
66
|
| macOS x64 | `muscat-pk-darwin-x64` |
|
|
60
67
|
| Windows x64 | `muscat-pk-win-x64.exe` |
|
|
61
68
|
|
|
62
|
-
Channel pointers:
|
|
69
|
+
Channel pointers: `.../muscat-plugin-kit/latest-version.txt` (stable), `.../muscat-plugin-kit/dev-version.txt` (dev).
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
# latest stable version string
|
|
66
|
-
https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/latest-version.txt
|
|
71
|
+
### Requirements
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
| Tool | Needed for |
|
|
74
|
+
|---|---|
|
|
75
|
+
| Node.js 20+ (or the binary) | the CLI, UI build |
|
|
76
|
+
| Docker | every backend runtime except `none` (`dev:back`, `dev`, `build`) |
|
|
77
|
+
| `uv` or Python 3.12 (optional) | IDE-only `.venv` for Python linting |
|
|
71
78
|
|
|
72
79
|
### Uninstall
|
|
73
80
|
|
|
74
|
-
Regardless of how it was installed, `muscat-pk` can remove itself:
|
|
75
|
-
|
|
76
81
|
```bash
|
|
77
|
-
muscat-pk uninstall
|
|
82
|
+
muscat-pk uninstall # works for npm and binary installs
|
|
83
|
+
npm uninstall -g @rainbow-robotics/muscat-plugin-kit # manual, npm
|
|
84
|
+
sudo rm -f /usr/local/bin/muscat-pk # manual, binary
|
|
78
85
|
```
|
|
79
86
|
|
|
80
|
-
|
|
87
|
+
---
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
# if installed via npm
|
|
84
|
-
npm uninstall -g @rainbow-robotics/muscat-plugin-kit
|
|
89
|
+
## 2. Quick start
|
|
85
90
|
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
```bash
|
|
92
|
+
muscat-pk new my-plugin # scaffold (asks author, email, description, type, package manager)
|
|
93
|
+
cd my-plugin
|
|
94
|
+
muscat-pk dev # UI preview + backend together
|
|
95
|
+
muscat-pk build # dist/ + distributable ZIP
|
|
88
96
|
```
|
|
89
97
|
|
|
98
|
+
Install the ZIP from the robot's Muscat plugin page.
|
|
99
|
+
|
|
90
100
|
---
|
|
91
101
|
|
|
92
|
-
##
|
|
102
|
+
## 3. Commands
|
|
103
|
+
|
|
104
|
+
| Command | Description |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `muscat-pk new [dir]` | Scaffold a plugin project |
|
|
107
|
+
| `muscat-pk type change` | Switch the backend type |
|
|
108
|
+
| `muscat-pk dev` | Local Muscat runtime + `dev:ui` + `dev:back` |
|
|
109
|
+
| `muscat-pk dev:ui` | UI preview in a mini-Muscat (preview-host) |
|
|
110
|
+
| `muscat-pk dev:back` | Backend only, in Docker, with hot reload |
|
|
111
|
+
| `muscat-pk build` | Build, lock Python deps, vendor wheels, ZIP |
|
|
112
|
+
| `muscat-pk update` | Update the kit, `plugin-ui` and `preview-host` |
|
|
113
|
+
| `muscat-pk sdk <list\|update [version]>` | Manage the `rainbow-rb-sdk` version |
|
|
114
|
+
| `muscat-pk storybook` | Browse the bundled rb-components Storybook |
|
|
115
|
+
| `muscat-pk mcp` | MCP server for AI coding agents |
|
|
116
|
+
| `muscat-pk uninstall` | Remove the CLI |
|
|
117
|
+
| `muscat-pk version` | Print the running version (`-v`, `--version`) |
|
|
118
|
+
| `muscat-pk completion [shell]` | Print a shell completion script |
|
|
119
|
+
|
|
120
|
+
`muscat-pk <command> --help` prints the options of a command.
|
|
121
|
+
|
|
122
|
+
### 3.1 `new`
|
|
93
123
|
|
|
94
124
|
```bash
|
|
95
|
-
muscat-pk new
|
|
96
|
-
|
|
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
|
+
| Option | Meaning |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `dir` | target directory (must be empty); asked when omitted |
|
|
133
|
+
| `--type` | backend type: `python` (default), `cpp`, `ros2` (rclpy), `ros2-cpp` (rclcpp) |
|
|
134
|
+
| `--pm` | front-end package manager; defaults to `yarn` inside a Yarn workspace, else `npm` |
|
|
135
|
+
| `--author` / `--email` / `--description` | manifest fields (email is required by the manifest) |
|
|
136
|
+
| `--yes` | no prompts; unspecified values use defaults |
|
|
137
|
+
| `--no-install` | skip `npm install` |
|
|
138
|
+
|
|
139
|
+
Python projects also get an IDE-only `.venv` (see [9.6](#96-ide-support-python)).
|
|
97
140
|
|
|
98
|
-
|
|
99
|
-
muscat-pk dev:ui # UI preview only
|
|
100
|
-
muscat-pk dev:back # backend only (Docker: python/cpp/ros2)
|
|
141
|
+
### 3.2 `type change`
|
|
101
142
|
|
|
102
|
-
|
|
143
|
+
```bash
|
|
144
|
+
muscat-pk type change [--to python|cpp|ros2|ros2-cpp] [--readme]
|
|
103
145
|
```
|
|
104
146
|
|
|
105
|
-
|
|
147
|
+
Rewrites `manifest.json` → `runtime` and adds the new backend's files. Files of the old type (`cpp/`, `ros2_ws/`, …) are left in place. `--readme` also replaces the README with the new type's.
|
|
106
148
|
|
|
107
|
-
|
|
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
|
+
| Command | What runs |
|
|
108
158
|
|---|---|
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `muscat-pk dev` | UI + backend dev server together |
|
|
113
|
-
| `muscat-pk dev:ui` | Mini-Muscat preview server |
|
|
114
|
-
| `muscat-pk dev:back` | Backend dev server (Docker, hot-reload) |
|
|
115
|
-
| `muscat-pk storybook` | Browse the bundled rb-components Storybook |
|
|
116
|
-
| `muscat-pk mcp` | Run an MCP server for AI coding agents (exposes rb-components/design-token/SDK docs) |
|
|
117
|
-
| `muscat-pk update` | Update the kit + plugin-ui + preview-host to the latest version; lists old-API UI code to fix |
|
|
118
|
-
| `muscat-pk sdk <list\|update>` | Manage the `rainbow-rb-sdk` version — python: `requirements.txt`, C++ (cpp / ros2-cpp): `find_package(rb_sdk <ver> EXACT REQUIRED)` in `CMakeLists.txt` |
|
|
119
|
-
| `muscat-pk version` | Print the installed CLI version (`-v` / `--version` also work) |
|
|
120
|
-
| `muscat-pk completion [shell]` | Print a shell completion script (`bash` / `zsh` / `fish` / `powershell`; defaults to `$SHELL`) |
|
|
159
|
+
| `dev` | a local Muscat runtime when no robot is given, then `dev:ui` and `dev:back` |
|
|
160
|
+
| `dev:ui` | the plugin's Vite dev server + preview-host at `http://localhost:4180/plugin-dev` (next free port) |
|
|
161
|
+
| `dev:back` | the backend in a Docker container matching the robot. Python restarts on `py/**/*.py` changes (watchfiles) |
|
|
121
162
|
|
|
122
|
-
|
|
163
|
+
- Robot IP comes from `--robot-ip`, `MUSCAT_ROBOT_HOST`, or a prompt (leave blank for local).
|
|
164
|
+
- Against a remote robot the plugin runs under a dev id (see [9.3](#93-several-developers-on-one-robot)); `--keep-id` uses the manifest id.
|
|
165
|
+
- The manifest is validated before start; undeclared listening ports stop the backend (see [5.6](#56-ports)).
|
|
123
166
|
|
|
124
|
-
|
|
125
|
-
|
|
167
|
+
### 3.4 `build`
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
muscat-pk build [--arch amd64|arm64|both]
|
|
171
|
+
```
|
|
126
172
|
|
|
127
|
-
|
|
128
|
-
project that has its own install, the CLI hands off to that copy. So the CLI,
|
|
129
|
-
the `vite` preset your `vite.config.ts` imports, and the `preview-host` that
|
|
130
|
-
`dev:ui` launches are always the single version the project pinned — a global
|
|
131
|
-
CLI that is newer or older than the project can't half-apply itself.
|
|
173
|
+
Steps:
|
|
132
174
|
|
|
133
|
-
|
|
175
|
+
1. Asks for a new `manifest.json` version (blank keeps the current one).
|
|
176
|
+
2. Validates the manifest.
|
|
177
|
+
3. `vite build` of every `contributes[].ui` entry → `dist/<entry>.js`.
|
|
178
|
+
4. Backend:
|
|
179
|
+
- python: pins unpinned lines of `requirements.txt` to the latest PyPI version, writes `dist/requirements.lock` (`uv pip compile`), and vendors wheels into `dist/wheels/` per arch.
|
|
180
|
+
- cpp: CMake build in Docker → `dist/<arch>/<entry>` plus `dist/<arch>/lib/librb_sdk.so`.
|
|
181
|
+
- ros2: colcon build in Docker → `dist/ros2_ws/install-<arch>/`.
|
|
182
|
+
5. Packs `dist/` into `<project dir>-<version>.zip` in the project root.
|
|
134
183
|
|
|
135
|
-
|
|
|
184
|
+
| Option | Meaning |
|
|
136
185
|
|---|---|
|
|
137
|
-
|
|
|
138
|
-
|
|
139
|
-
|
|
186
|
+
| `--arch` | target arch; asked in a TTY, `both` otherwise |
|
|
187
|
+
|
|
188
|
+
### 3.5 `update`
|
|
140
189
|
|
|
141
190
|
```bash
|
|
142
|
-
muscat-pk update # this project only
|
|
191
|
+
muscat-pk update # this project only (when it has a local install)
|
|
143
192
|
muscat-pk update --global # this project + the global CLI
|
|
144
|
-
muscat-pk update dev # follow the dev channel
|
|
145
|
-
muscat-pk version # which version is actually running here
|
|
193
|
+
muscat-pk update dev # follow the dev channel
|
|
146
194
|
```
|
|
147
195
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
196
|
+
| Where you run it | What it updates |
|
|
197
|
+
|---|---|
|
|
198
|
+
| Inside a project with a local install | that project's `muscat-plugin-kit` + `plugin-ui` |
|
|
199
|
+
| Same, with `--global` | the above + the global CLI |
|
|
200
|
+
| Anywhere else | the global CLI (and local deps if inside a project) |
|
|
152
201
|
|
|
153
|
-
|
|
154
|
-
registry to resolve the base image tag on every run — even when every layer is
|
|
155
|
-
cached — so rebuilding an image you already have would break `dev` with no
|
|
156
|
-
network. Once an image exists locally, `muscat-pk` skips the build entirely.
|
|
202
|
+
After updating it lists UI code that still uses removed APIs. Fix them (or run `check_project` through `muscat-pk mcp`).
|
|
157
203
|
|
|
158
|
-
|
|
204
|
+
### 3.6 `sdk`
|
|
159
205
|
|
|
160
206
|
```bash
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# on the offline machine
|
|
165
|
-
gunzip -c muscat-pk-images.tar.gz | docker load
|
|
207
|
+
muscat-pk sdk list # published rainbow-rb-sdk versions
|
|
208
|
+
muscat-pk sdk update [version] # latest when omitted
|
|
166
209
|
```
|
|
167
210
|
|
|
168
|
-
|
|
211
|
+
- python: rewrites every `rainbow-rb-*==` pin in `requirements.txt` together (they pin each other exactly).
|
|
212
|
+
- C++ (`cpp`, `ros2-cpp`): rewrites `find_package(rb_sdk <ver> EXACT REQUIRED)` in `CMakeLists.txt`.
|
|
213
|
+
|
|
214
|
+
### 3.7 `storybook`, `mcp`, `completion`, `version`
|
|
169
215
|
|
|
170
|
-
|
|
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` prints a Tab-completion script for your shell — once registered, `muscat-pk d<Tab>` offers `dev`, `dev:ui`, `dev:back`. It only prints the script; append it to your shell's rc file as below. `[shell]` is `bash`, `zsh`, `fish` or `powershell`, defaulting to `$SHELL`. Windows `cmd.exe` has no completion hook — use PowerShell, Git Bash or WSL. Optional convenience; nothing depends on it.
|
|
171
224
|
|
|
172
|
-
| OS / shell |
|
|
225
|
+
| OS / shell | Completion setup |
|
|
173
226
|
|---|---|
|
|
174
227
|
| macOS (zsh) | `muscat-pk completion zsh >> ~/.zshrc && source ~/.zshrc` |
|
|
175
228
|
| Linux (bash) | `muscat-pk completion bash >> ~/.bashrc && source ~/.bashrc` |
|
|
176
229
|
| fish | `muscat-pk completion fish > ~/.config/fish/completions/muscat-pk.fish` |
|
|
177
|
-
|
|
|
230
|
+
| PowerShell | `muscat-pk completion powershell >> $PROFILE; . $PROFILE` |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 4. Project layout
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
my-plugin/
|
|
238
|
+
├─ ui/
|
|
239
|
+
│ ├─ content.tsx # UI entry (one file per contributes[].ui), `export default App`
|
|
240
|
+
│ └─ style.css # auto-loaded for every entry
|
|
241
|
+
├─ py/main.py # python backend entry (runtime.entry)
|
|
242
|
+
├─ cpp/ # cpp backend (CMake) — cpp type
|
|
243
|
+
├─ ros2_ws/ # colcon workspace — ros2 / ros2-cpp type
|
|
244
|
+
├─ assets/icon.svg
|
|
245
|
+
├─ manifest.json
|
|
246
|
+
├─ requirements.txt # python deps (pinned on build)
|
|
247
|
+
├─ package.json # UI deps (not shipped)
|
|
248
|
+
├─ vite.config.ts # must use the muscat() preset
|
|
249
|
+
└─ README.md # copied into dist/
|
|
250
|
+
```
|
|
178
251
|
|
|
179
|
-
|
|
252
|
+
`vite.config.ts`:
|
|
180
253
|
|
|
181
|
-
|
|
254
|
+
```ts
|
|
255
|
+
import { defineConfig } from "vite";
|
|
256
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
257
|
+
import { muscat } from "@rainbow-robotics/muscat-plugin-kit/vite";
|
|
182
258
|
|
|
183
|
-
|
|
184
|
-
|
|
259
|
+
export default defineConfig({ plugins: [tailwindcss(), muscat()] });
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The ZIP contains only the plugin's own code and assets. `@rainbow-robotics/plugin-ui`, `react` and `react-dom` come from the host's import map.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 5. manifest.json
|
|
267
|
+
|
|
268
|
+
Validated by `muscat-pk dev`, `dev:back` and `build`.
|
|
269
|
+
|
|
270
|
+
### 5.1 Full example
|
|
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" }, "ui": "content", "opens": "popover" }
|
|
287
|
+
],
|
|
288
|
+
"variables": [
|
|
289
|
+
{ "name": "RBP_grip_force", "type": "number", "init": 0.5, "description": { "en": "Gripper force (0–1)" } }
|
|
290
|
+
],
|
|
291
|
+
"ports": [28600],
|
|
292
|
+
"runtime": { "type": "python", "entry": "main.py" }
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 5.2 Top-level fields
|
|
297
|
+
|
|
298
|
+
| Field | Required | Rule |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| `id` | yes | reverse-domain: `.`-separated parts of lowercase letters/digits joined by single `-`, each starting with a letter (`com.example.my-plugin`). No `_` |
|
|
301
|
+
| `name`, `version`, `minMuscatVersion` | yes | non-empty strings |
|
|
302
|
+
| `author`, `email` | yes | non-empty strings |
|
|
303
|
+
| `company` | no | string |
|
|
304
|
+
| `description` | yes | string or i18n object |
|
|
305
|
+
| `icons` | yes | SVG path string, or `{ "16", "48", "128" }` for PNG/JPG (all three) |
|
|
306
|
+
| `targetModels` / `targetCategorys` | at least one | `"all"` or a non-empty array. Categories: `manipulate`, `amr`, `rby1`, `vision` |
|
|
307
|
+
| `contributes` | yes | see 5.3 |
|
|
308
|
+
| `variables` | no | see 5.5 |
|
|
309
|
+
| `ports` | no | see 5.6 |
|
|
310
|
+
| `runtime` | yes | see 5.7 |
|
|
311
|
+
|
|
312
|
+
i18n object: `en` is required; allowed keys `ko`, `en`, `ja`, `zh`, `vi`, `th`, `es`.
|
|
313
|
+
|
|
314
|
+
### 5.3 `contributes`
|
|
315
|
+
|
|
316
|
+
| Field | Rule |
|
|
317
|
+
|---|---|
|
|
318
|
+
| `point` | `toolbar`, `lnb`, `move.panel`, `program.panel`, `program.monitor` |
|
|
319
|
+
| `ui` | UI entry name under `ui/` without extension (default `content`); unique across entries |
|
|
320
|
+
| `label` | required, string or i18n object |
|
|
321
|
+
| `icon` | icon path |
|
|
322
|
+
| `opens` | toolbar only: `popover`, `panel`, `page` |
|
|
323
|
+
| `commandInfo` | required for `program.panel` (5.4) |
|
|
324
|
+
|
|
325
|
+
At least one `toolbar` entry is required.
|
|
326
|
+
|
|
327
|
+
| Point | Where it shows |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `toolbar` | header toolbar button; opens as popover / panel / page |
|
|
330
|
+
| `lnb` | left navigation |
|
|
331
|
+
| `move.panel` | Move page panel |
|
|
332
|
+
| `program.panel` | a custom command in the Program page's step tree; its `ui` is the step's edit form |
|
|
333
|
+
| `program.monitor` | a tab in the Program page's monitor panel (next to General / Digital I/O / Monitor / Log), rendered per task |
|
|
334
|
+
|
|
335
|
+
### 5.4 `commandInfo` (`program.panel`)
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"point": "program.panel",
|
|
340
|
+
"ui": "program",
|
|
341
|
+
"label": { "en": "My Step" },
|
|
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
|
+
| Field | Type | Meaning |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `name` | string, required | command name in the tree |
|
|
354
|
+
| `icon` | string, required | icon path |
|
|
355
|
+
| `iconClassName` | string | extra icon class |
|
|
356
|
+
| `funcName` | string | function the executor calls (`rb_program_sdk.plugin_execute` for Python backends) |
|
|
357
|
+
| `args` | object | default step args, passed with `plugin_id` to the backend |
|
|
358
|
+
| `disabled`, `syncGroupId`, `disableCopy`, `disableDelete`, `requireChildren`, `requireParent` | boolean | tree editing rules |
|
|
359
|
+
| `allowChildren`, `allowParents`, `notAllowChildren` | string[] | allowed / forbidden neighbour commands |
|
|
360
|
+
| `childrenCommand` | string | command created as a child |
|
|
361
|
+
|
|
362
|
+
### 5.5 `variables` (PyFM)
|
|
363
|
+
|
|
364
|
+
Variables the plugin exposes to PyFM programs. The Program page's variable picker lists only declared ones.
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{ "name": "RBP_target_pose", "type": "array", "init": [0, 0, 0], "description": { "en": "Target pose" } }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
| Field | Required | Rule |
|
|
371
|
+
|---|---|---|
|
|
372
|
+
| `name` | yes | `RBP_` + letters/digits/underscore; unique per plugin |
|
|
373
|
+
| `type` | yes | `number`, `string`, `boolean`, `array` |
|
|
374
|
+
| `init` | yes | initial value matching `type` |
|
|
375
|
+
| `description` | no | string or i18n object — write one, the UI shows nothing otherwise |
|
|
376
|
+
|
|
377
|
+
### 5.6 `ports`
|
|
378
|
+
|
|
379
|
+
A backend that opens a TCP listener must declare every port:
|
|
185
380
|
|
|
186
381
|
```json
|
|
187
382
|
{ "ports": [28600, 28601] }
|
|
@@ -189,80 +384,635 @@ A plugin backend that opens a TCP listener must declare every such port in
|
|
|
189
384
|
|
|
190
385
|
| Rule | Enforced by |
|
|
191
386
|
|---|---|
|
|
192
|
-
|
|
|
193
|
-
| Reserved
|
|
194
|
-
|
|
|
195
|
-
| No two enabled plugins
|
|
387
|
+
| Only `28600-28999`, `number[]` | manifest validation |
|
|
388
|
+
| Reserved ports rejected: `502`, `2222`, `4840`, `5170-5179`, `8000-8999`, `10000`, `30004`, `34964`, `44818`, `50000-50005`, `50051` | manifest validation |
|
|
389
|
+
| Every listening port must be declared | `dev` / `dev:back` runtime monitor stops the backend on the first undeclared port |
|
|
390
|
+
| No two enabled plugins share a port | the robot, at install (HTTP 409) |
|
|
196
391
|
|
|
197
|
-
The
|
|
198
|
-
range is invisible to both the monitor and the robot's conflict check, so it
|
|
199
|
-
would only surface as a collision after deployment.
|
|
392
|
+
The kit's own dev ports (gateway `10000`, the Zenoh dev peer port) are excluded. Under `muscat-pk dev` declared ports are published on `127.0.0.1`; a busy host port is skipped with a warning.
|
|
200
393
|
|
|
201
|
-
|
|
202
|
-
peer port) is excluded from the check — it isn't the plugin's traffic.
|
|
394
|
+
### 5.7 `runtime`
|
|
203
395
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
396
|
+
| runtime | Source | Required fields |
|
|
397
|
+
|---|---|---|
|
|
398
|
+
| `{ "type": "none" }` | — | UI-only plugin |
|
|
399
|
+
| `{ "type": "python", "entry": "main.py" }` | `py/` | `entry` |
|
|
400
|
+
| `{ "type": "cpp", "entry": "<executable>" }` | `cpp/` | `entry` = built executable name |
|
|
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/` | same |
|
|
207
403
|
|
|
208
|
-
|
|
404
|
+
---
|
|
209
405
|
|
|
210
|
-
|
|
211
|
-
`manifest.json` — the Muscat UI lists only declared variables in its variable
|
|
212
|
-
picker:
|
|
406
|
+
## 6. Plugin UI SDK (`useRb`)
|
|
213
407
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
408
|
+
Package `@rainbow-robotics/plugin-ui`:
|
|
409
|
+
|
|
410
|
+
| Import | Contents |
|
|
411
|
+
|---|---|
|
|
412
|
+
| `@rainbow-robotics/plugin-ui/react` | Muscat components (`RB*`), `useRb`, `useRbFormData`, `useRbSave`, `useRbStorage`, `createRbStore` |
|
|
413
|
+
| `@rainbow-robotics/plugin-ui/sdk` | the same hooks without the components, `RbSdk` / `RbStore` / `RbSetState` types |
|
|
414
|
+
|
|
415
|
+
### 6.1 Entry component
|
|
416
|
+
|
|
417
|
+
Each top-level `ui/*.tsx` default-exports a React component. The host mounts it and provides `rb` through context.
|
|
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>;
|
|
220
426
|
}
|
|
221
427
|
```
|
|
222
428
|
|
|
223
|
-
|
|
429
|
+
Use plugin-ui components instead of native `<button>`, `<input>`, `<select>`, `<textarea>`, `<table>`, `<dialog>`. Common mappings: button → `RBLabelButton` / `RBIconButton`, text input → `RBTextInput` / `RBTextField`, number → `RBSideNumberInput`, select → `RBSelectBox` + `Option`, checkbox → `RBCheckBox`, switch → `RBSwitch`, slider → `RBSlider`, table → `RBTable`, modal → `RBModal`, tabs → `RBTabs`. `muscat-pk storybook` and `muscat-pk mcp` show props and examples.
|
|
430
|
+
|
|
431
|
+
### 6.2 Messaging with the backend
|
|
432
|
+
|
|
433
|
+
| UI | Backend (Python) | Pattern |
|
|
224
434
|
|---|---|---|
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
435
|
+
| `rb.pub(topic, payload)` | `@plugin.subscribe(topic)` | fire-and-forget UI → backend |
|
|
436
|
+
| `rb.sub(topic, handler)` | `plugin.publish(topic, payload)` | backend → UI stream |
|
|
437
|
+
| `rb.action(topic, payload?)` | `@plugin.serve(topic)` | UI request, backend reply |
|
|
438
|
+
| `rb.serve(topic, handler)` | `plugin.call(topic, payload=...)` | backend request, UI reply |
|
|
229
439
|
|
|
230
|
-
|
|
440
|
+
```tsx
|
|
441
|
+
const rb = useRb();
|
|
231
442
|
|
|
232
|
-
|
|
443
|
+
await rb.pub("event", { clicked: true });
|
|
233
444
|
|
|
234
|
-
|
|
445
|
+
const sub = await rb.sub<{ ready: boolean }>("state", (s) => setReady(s.ready));
|
|
446
|
+
// cleanup: sub.undeclare()
|
|
235
447
|
|
|
236
|
-
|
|
448
|
+
const res = await rb.action<{ ok: boolean }, { width: number }>("grip", { width: 20 });
|
|
237
449
|
|
|
450
|
+
await rb.serve("tool/ping", (payload) => ({ pong: payload }));
|
|
238
451
|
```
|
|
239
|
-
|
|
452
|
+
|
|
453
|
+
- Topics are scoped to the plugin id: `"state"` → `{pluginId}/state`. A leading `/` makes it absolute (`"/broadcast/x"`).
|
|
454
|
+
- All four take generics for payload / result types.
|
|
455
|
+
- Unsubscribe in a `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]);
|
|
240
463
|
```
|
|
241
464
|
|
|
242
|
-
|
|
465
|
+
### 6.3 Host dialogs and toast
|
|
243
466
|
|
|
244
|
-
|
|
467
|
+
| API | Resolves |
|
|
468
|
+
|---|---|
|
|
469
|
+
| `rb.toast({ title?, message, type?, actionLabel?, handleActionClick?, closeOnActionClick?, duration?, onClose? })` | — (`message`: string or ReactElement; `type`: `info`, `danger`, `warning`, `caution`, `success`) |
|
|
470
|
+
| `rb.alert({ title?, message, buttonText? })` | when closed |
|
|
471
|
+
| `rb.confirm({ title?, message, buttonText?: { yes, no } })` | `true` / `false` |
|
|
472
|
+
| `rb.prompt({ title?, message, value?, placeholder?, buttonText?: { yes, no } })` | entered string, or `undefined` when cancelled |
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
if (await rb.confirm({ title: "Run", message: "Start the backend action?" })) {
|
|
476
|
+
await rb.action("run");
|
|
477
|
+
rb.toast({ message: "Started", type: "success" });
|
|
478
|
+
}
|
|
479
|
+
```
|
|
245
480
|
|
|
246
|
-
|
|
481
|
+
### 6.4 Program page (`rb.program`)
|
|
247
482
|
|
|
248
|
-
|
|
483
|
+
| API | Meaning |
|
|
484
|
+
|---|---|
|
|
485
|
+
| `rb.program.getSteps(scope?)` | main step tree of the program open on the Program page |
|
|
486
|
+
| `rb.program.setSteps(steps, scope?)` | replace it — an unsaved edit the user then saves |
|
|
487
|
+
| `rb.program.getCatalog(scope?)` | the loaded robot's command catalog (names, default args, nesting rules) |
|
|
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>; // step-level fields, e.g. SubTask's subTaskType
|
|
495
|
+
children?: PluginProgramStep[];
|
|
496
|
+
};
|
|
497
|
+
type PluginProgramScope = { taskId?: string; robotModel?: string };
|
|
498
|
+
```
|
|
249
499
|
|
|
250
|
-
|
|
500
|
+
Without `scope` the host uses the first main task. In a `program.monitor` tab `rb.data` is `{ point: "program.monitor", taskId, robotModel }` — pass it as `scope` so a multi-component robot hits the right task.
|
|
251
501
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
502
|
+
### 6.5 Step form save/restore (`program.panel`)
|
|
503
|
+
|
|
504
|
+
| Hook | Meaning |
|
|
505
|
+
|---|---|
|
|
506
|
+
| `useRbFormData<T>()` | values saved in this step, `undefined` for a new step |
|
|
507
|
+
| `useRbSave(fn)` | registers the getter the host calls on Save; always sees current state. Return value must be 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
|
+
);
|
|
260
527
|
}
|
|
261
528
|
```
|
|
262
529
|
|
|
263
|
-
|
|
530
|
+
The saved `args` reach the backend's `on_execute(args)` (see [7.3](#73-pluginprogramcontext--program-command)).
|
|
531
|
+
|
|
532
|
+
### 6.6 Browser storage (`rb.storage`, `useRbStorage`, `createRbStore`)
|
|
533
|
+
|
|
534
|
+
Keep browser-side plugin data only here. Everything is namespaced `muscat-plugin:<pluginId>`, and the host deletes it when the plugin is uninstalled — data stored any other way (raw `localStorage`, your own IndexedDB) is never cleaned up.
|
|
535
|
+
|
|
536
|
+
| Backend | Use |
|
|
537
|
+
|---|---|
|
|
538
|
+
| `rb.storage` (= `rb.storage.indexedDB`) | default; structured values (objects, arrays, `Date`, `Blob`, …) |
|
|
539
|
+
| `rb.storage.local` | `localStorage`, JSON values, survives reloads |
|
|
540
|
+
| `rb.storage.session` | `sessionStorage`, JSON values, gone when the tab closes |
|
|
541
|
+
| `rb.storage.cookie` | ~4 KB per value and sent with every request to the host — tiny values only |
|
|
542
|
+
|
|
543
|
+
Every backend has the same API:
|
|
544
|
+
|
|
545
|
+
| Method | Meaning |
|
|
546
|
+
|---|---|
|
|
547
|
+
| `get<T>(key)` | value, or `undefined` |
|
|
548
|
+
| `set(key, value)` | save |
|
|
549
|
+
| `delete(key)` | remove |
|
|
550
|
+
| `keys()` | this plugin's keys |
|
|
551
|
+
| `subscribe(listener, key?)` | `({ kind, key, value }) => void` on every change made through this API — this screen, other screens of the plugin (toolbar panel, Program tab, …) and other browser tabs. `value` is `undefined` after a delete. Returns the unsubscribe function |
|
|
552
|
+
| `getItem` / `setItem` / `removeItem` | zustand `StateStorage` (raw strings) |
|
|
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` — one stored value as 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) saves; setDraft(undefined) deletes
|
|
571
|
+
return <RBTextInput value={draft?.name ?? ""} handleChange={(name: string) => void setDraft({ name })} />;
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
- `useRbStorage<T>(key, { kind?, initial? })` → `[value, setValue]`. `kind` defaults to `"indexeddb"`.
|
|
576
|
+
- Re-renders when the value changes anywhere (other screens, other tabs).
|
|
577
|
+
- `value` is `initial` until the stored value has loaded.
|
|
578
|
+
|
|
579
|
+
#### `createRbStore` — persisted zustand-style store
|
|
580
|
+
|
|
581
|
+
Define once at module level, use from any screen of the plugin:
|
|
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); // re-renders only when speed changes
|
|
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
|
+
| Member | Meaning |
|
|
611
|
+
|---|---|
|
|
612
|
+
| `createRbStore<T>(init, { name, storage? })` | `init(set, get)` returns initial state + actions. `name` is the storage key; `storage` is a backend kind (default `"indexeddb"`) |
|
|
613
|
+
| `useStore()` / `useStore(selector)` | whole state / a slice (re-renders only when the slice changes) |
|
|
614
|
+
| `useStore.getState()` | current state, outside React |
|
|
615
|
+
| `useStore.setState(partial \| (s) => partial)` | merge a change, outside React |
|
|
616
|
+
| `useStore.subscribe((next, prev) => …)` | listen outside React; returns unsubscribe |
|
|
617
|
+
|
|
618
|
+
- State changes only through `set` / `setState`. Every change is saved and re-renders every screen and tab using the store.
|
|
619
|
+
- Functions (actions) are not saved; only data fields are.
|
|
620
|
+
- The saved data loads when the first component using the store mounts (the plugin id is known only then). Before that `getState()` returns the initial values.
|
|
621
|
+
- Changes are merged shallowly: set a new array/object (`[...s.points, p]`) instead of mutating it.
|
|
622
|
+
|
|
623
|
+
#### Using zustand `persist` directly
|
|
624
|
+
|
|
625
|
+
Every backend is a 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` is then a dependency of your plugin; `createRbStore` needs nothing extra.
|
|
633
|
+
|
|
634
|
+
### 6.7 Other
|
|
635
|
+
|
|
636
|
+
| API | Meaning |
|
|
637
|
+
|---|---|
|
|
638
|
+
| `rb.data` | host-provided mount data (`program.monitor` scope, saved form values) |
|
|
639
|
+
| `rb.rosPeers.get()` | PCs allowed to reach this plugin's ROS 2 nodes |
|
|
640
|
+
| `rb.rosPeers.set(ips)` | save the list and restart the backend; rejects an invalid IP |
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 7. Python plugin SDK
|
|
645
|
+
|
|
646
|
+
Python backends (`runtime.type: "python"`, entry `py/main.py`) use `rainbow-rb-sdk`, pinned in `requirements.txt` and changed with `muscat-pk sdk update`. The robot's plugin container provides the SDK at runtime.
|
|
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
|
+
Which API depends on the contribution point:
|
|
655
|
+
|
|
656
|
+
| Contribution point | Backend |
|
|
657
|
+
|---|---|
|
|
658
|
+
| `toolbar` / `lnb` / `move.panel` / `program.monitor` | `RBPluginCall` + robot SDKs (`RBManipulateSDK`, `RBAmrSDK`) called directly. No stop/pause/variable context |
|
|
659
|
+
| `program.panel` | `PluginProgramContext` — runs as a program step, tied to stop/pause and program variables |
|
|
660
|
+
|
|
661
|
+
### 7.1 `RBPluginCall` — UI ↔ backend
|
|
662
|
+
|
|
663
|
+
Internal only (UI ↔ backend, plugin ↔ plugin); not reachable from outside Muscat. The plugin id comes from env `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
|
+
| Member | Meaning |
|
|
683
|
+
|---|---|
|
|
684
|
+
| `serve(keyexpr, handler=None)` | request/response handler; decorator or direct call. Return value = reply |
|
|
685
|
+
| `subscribe(topic, callback=None)` | receive publishes |
|
|
686
|
+
| `publish(topic, payload=None)` | push to subscribers |
|
|
687
|
+
| `call(keyexpr, *, payload, timeout=3.0)` | request the other side and wait |
|
|
688
|
+
| `unserve(keyexpr=None)` | remove one / all serve handlers |
|
|
689
|
+
| `unsubscribe(topic=None, callback=None)` | remove subscriptions |
|
|
690
|
+
| `plugin_id` | the plugin id in use (dev id under `muscat-pk dev`) |
|
|
691
|
+
| `connected` | transport connected |
|
|
692
|
+
| `data_dir` | persistent data directory, created on access. Survives updates and restarts; removed with the plugin. The install directory (cwd) is replaced on every update — keep state, caches and settings here |
|
|
693
|
+
| `router` | external endpoints (7.2) |
|
|
694
|
+
|
|
695
|
+
Payloads must be JSON-serialisable. Handlers may be sync or async.
|
|
696
|
+
|
|
697
|
+
### 7.2 `plugin.router` — external endpoints
|
|
698
|
+
|
|
699
|
+
Exposes `common/plugin/{plugin_id}/{path}` over REST, Socket.IO, TCP, MQTT and WebRTC. The running Muscat (`common`) picks it up without a restart; it disappears when the plugin stops. Requires `rainbow-rb-sdk` ≥ 0.0.9.dev5 on the robot (older: `AttributeError`).
|
|
700
|
+
|
|
701
|
+
```python
|
|
702
|
+
@plugin.router.on("/hello") # every 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 only
|
|
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 | Meaning | External side |
|
|
720
|
+
|---|---|---|
|
|
721
|
+
| `router.on(path, explicit_routes=None)` | request/response | calls `common/plugin/{id}/{path}` (REST: `POST`) |
|
|
722
|
+
| `router.subscribe(topic, explicit_events=None)` | receive external publishes | publishes to `common/plugin/{id}/{topic}` |
|
|
723
|
+
| `router.emit(topic, payload=None)` | push to external subscribers | subscribes to `common/plugin/{id}/{topic}` |
|
|
724
|
+
|
|
725
|
+
Handler contract:
|
|
726
|
+
|
|
727
|
+
- Signature `(payload, meta)`, `(payload)` or `()` — only declared arguments are injected. Sync or async.
|
|
728
|
+
- `payload`: what the caller sent (REST: JSON body merged with query params).
|
|
729
|
+
- `meta`: `{"transport": "api"|"socket"|"tcp"|"mqtt"|"webrtc", "principal": dict | None}`. With the login policy on, `principal` is the token subject. Role checks are the plugin's job.
|
|
730
|
+
- Return value = response body (JSON). `None` → empty.
|
|
731
|
+
- `raise PluginRouteError(code, message)` sets the status; any other exception → 500 with `str(exc)`.
|
|
732
|
+
- `subscribe` handlers take `(payload)` and return nothing.
|
|
733
|
+
|
|
734
|
+
Rules:
|
|
735
|
+
|
|
736
|
+
- Write only the path; the SDK adds `plugin_id`. Multi-segment paths work (`/tool/echo`). Declaring a path twice → `ValueError`.
|
|
737
|
+
- `explicit_routes` / `explicit_events`: `None` = every transport. Names `api | socket | tcp | mqtt | webrtc`; `subscribe` accepts only `tcp | mqtt | webrtc`. Unknown → `ValueError`. A disallowed transport gets 404.
|
|
738
|
+
- `on` and `subscribe` may share a name.
|
|
739
|
+
|
|
740
|
+
Calling from outside:
|
|
741
|
+
|
|
742
|
+
| Transport | How |
|
|
743
|
+
|---|---|
|
|
744
|
+
| REST | `POST /common/plugin/{id}/{path}`, JSON body |
|
|
745
|
+
| Socket.IO | emit `common/plugin/{id}/{path}`; reply in the ack |
|
|
746
|
+
| TCP | topic `common/plugin/{id}/{path}` request / publish |
|
|
747
|
+
| MQTT | request `common/plugin/{id}/{path}/__req__`; publish `common/plugin/{id}/{topic}` |
|
|
748
|
+
| WebRTC | data channel topic `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
|
+
# under muscat-pk dev (the dev id is printed at start)
|
|
754
|
+
curl -s -X POST http://localhost:3000/common/plugin/<dev-id>/hello -H 'Content-Type: application/json' -d '{}'
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
| Situation | Status |
|
|
758
|
+
|---|---|
|
|
759
|
+
| No such path / plugin down / transport not allowed | 404 |
|
|
760
|
+
| No answer within 10 s | 504 |
|
|
761
|
+
| `common` ↔ gateway not connected | 503 |
|
|
762
|
+
| Handler raised | 500 (`PluginRouteError` → its code) |
|
|
763
|
+
|
|
764
|
+
Auth: with the login policy on a token is required (any role); REST uses the same Bearer token as other Muscat APIs. Off: no token.
|
|
765
|
+
|
|
766
|
+
Known limits:
|
|
767
|
+
|
|
768
|
+
- A path named `install` can't be called over REST (taken by the registry install route).
|
|
769
|
+
- MQTT echoes to the sender: `emit` + `subscribe` on the same topic receives its own push over `mqtt`. Drop `mqtt` from `explicit_events` or use separate topics.
|
|
770
|
+
- After restarting the plugin, external calls may return 504 for up to a minute, then 404, until the gateway drops the old socket.
|
|
771
|
+
|
|
772
|
+
### 7.3 `PluginProgramContext` — program command
|
|
773
|
+
|
|
774
|
+
Used only for `program.panel`. When the program reaches the step, the executor calls `commandInfo.funcName` with `plugin_id` + the step args, and the SDK calls your `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) # new variable → RBP_ required
|
|
787
|
+
|
|
788
|
+
for _ in range(100):
|
|
789
|
+
await ctx.check_stop() # stop → CancelledError, pause → waits until resumed
|
|
790
|
+
await asyncio.sleep(0.5)
|
|
791
|
+
|
|
792
|
+
await ctx.done() # always; the step never finishes otherwise
|
|
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) # ignored when no program runs
|
|
803
|
+
|
|
804
|
+
async def main() -> None:
|
|
805
|
+
asyncio.create_task(background())
|
|
806
|
+
await ctx.start() # stays resident
|
|
807
|
+
|
|
808
|
+
asyncio.run(main())
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
| Member | Meaning |
|
|
812
|
+
|---|---|
|
|
813
|
+
| `on_execute(fn)` | decorator; called once per execution of this step with the step args |
|
|
814
|
+
| `done()` | step finished — always call |
|
|
815
|
+
| `check_stop(robot_model=None)` | stop → `CancelledError`, pause → waits. Call every loop iteration |
|
|
816
|
+
| `get_variable(name, robot_model=None, plugin_id=None)` | read; `None` when no program runs |
|
|
817
|
+
| `set_variable(name, value)` | write to every running main task; ignored when no program runs |
|
|
818
|
+
| `executor_state(robot_model=None)` | state string for a model, or `{model: state}` for all |
|
|
819
|
+
| `is_paused(robot_model=None)` / `is_stopped(robot_model=None)` | state checks |
|
|
820
|
+
| `on_executor_state_change(fn)` | decorator; `(robot_model, state)` once per change |
|
|
821
|
+
| `start()` / `stop()` / `pause()` | lifecycle |
|
|
822
|
+
| `in_execute`, `exec_robot_model`, `plugin_id`, `started`, `control_topic` | properties |
|
|
823
|
+
|
|
824
|
+
#### Variables
|
|
825
|
+
|
|
826
|
+
- Existing variable: written by name as-is — exchanging values with the program is the point.
|
|
827
|
+
- New variable: must start with `RBP_` (`RuntimeError` otherwise), inside or outside a step. Use `RBP_{plugin}_{name}` to avoid collisions; `RBP_` alone doesn't prevent two plugins overwriting each other.
|
|
828
|
+
- Declare in `manifest.json` → `variables` what the Program page should offer (5.5).
|
|
829
|
+
|
|
830
|
+
`set_variable` targets every running main task:
|
|
831
|
+
|
|
832
|
+
| When | Own program | Other robots' main tasks |
|
|
833
|
+
|---|---|---|
|
|
834
|
+
| During own step | immediate | at their next step boundary |
|
|
835
|
+
| Outside a step | next step boundary | same |
|
|
836
|
+
|
|
837
|
+
`get_variable` options:
|
|
838
|
+
|
|
839
|
+
```python
|
|
840
|
+
await ctx.get_variable("counter") # first value across main tasks
|
|
841
|
+
await ctx.get_variable("counter", robot_model="C500920") # one robot
|
|
842
|
+
await ctx.get_variable("RBP_TEST", plugin_id="A.plugin") # what that plugin last wrote
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
Each variable keeps a per-plugin record (`meta.writers`), so a colliding name is still recoverable: `get_variable(..., plugin_id=...)` from the SDK, `get_var("A.plugin", "RBP_TEST")` in a program expression. A write can rarely be lost when it races the worker's flush — re-check important values.
|
|
846
|
+
|
|
847
|
+
#### Executor state
|
|
848
|
+
|
|
849
|
+
States: `idle | running | paused | waiting | stopped | post_start | completed | error`. A model with no running task is `None` / absent.
|
|
850
|
+
|
|
851
|
+
- Published every 0.8 s on `muscat/executor/state` — up to ~1 s latency.
|
|
852
|
+
- With `IS_DEV=true` there is no publish; always `None`.
|
|
853
|
+
- `on_executor_state_change` fires once per per-model change; handler exceptions are only logged.
|
|
264
854
|
|
|
265
|
-
|
|
855
|
+
Scope of `check_stop` / `is_paused` / `is_stopped`:
|
|
856
|
+
|
|
857
|
+
1. `robot_model` given → that model
|
|
858
|
+
2. omitted inside `on_execute` → the model executing this step
|
|
859
|
+
3. omitted elsewhere → all models
|
|
860
|
+
|
|
861
|
+
### 7.4 Robot SDKs from the backend
|
|
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
|
+
Add your own safety checks before moving the robot. Exact signatures: `muscat-pk mcp` (`search_rb_sdk`, `get_rb_sdk_api`).
|
|
872
|
+
|
|
873
|
+
### 7.5 Remote dev (laptop → robot)
|
|
874
|
+
|
|
875
|
+
```bash
|
|
876
|
+
export MUSCAT_ROBOT_HOST=192.168.1.100
|
|
877
|
+
export MUSCAT_ROBOT_TOKEN=<service token> # only when the robot's login policy is on
|
|
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() # after connect() — connect() reopens the session and drops earlier handlers
|
|
890
|
+
...
|
|
891
|
+
if host:
|
|
892
|
+
await manipulate.disconnect()
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
The scaffolded `py/main.py` already follows this.
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## 8. Other backend runtimes (C++ / ROS 2)
|
|
900
|
+
|
|
901
|
+
All non-`none` runtimes run in Docker during `dev:back`, `dev` and `build`.
|
|
902
|
+
|
|
903
|
+
### 8.1 C++ (`cpp`)
|
|
904
|
+
|
|
905
|
+
- Source in `cpp/`, CMake build in Docker against the C++ SDK (apt `rainbow-rb-sdk-dev`).
|
|
906
|
+
- CMake: `find_package(rb_sdk <ver> EXACT REQUIRED)` + `target_link_libraries(<target> rb_sdk::rb_sdk)` (C++20).
|
|
907
|
+
- Headers: `<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
|
+
- Dynamic link; `build` copies `librb_sdk.so` into `dist/<arch>/lib/`.
|
|
909
|
+
|
|
910
|
+
Building outside the container (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
|
+
- Source in `ros2_ws/src/<package>/`; `runtime.package` = colcon package, `runtime.launch` = node executable.
|
|
921
|
+
- Relative topic names live under the plugin's namespace: id with `.` → `/`, `-` → `_` (`com.example.my-plugin` → `/com/example/my_plugin`). Use absolute names (`/cmd_vel`) for shared topics.
|
|
922
|
+
- `RBPluginCall` works in an rclpy node too.
|
|
923
|
+
- ROS discovery stays on the robot (`ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`). Allow external PCs from the UI with `rb.rosPeers.set([...])`; Muscat restarts the backend with `ROS_STATIC_PEERS`. In dev the list is in `.muscat-dev/ros-peers.json`.
|
|
924
|
+
|
|
925
|
+
---
|
|
926
|
+
|
|
927
|
+
## 9. Development environment
|
|
928
|
+
|
|
929
|
+
### 9.1 Local Muscat runtime
|
|
930
|
+
|
|
931
|
+
Without a robot IP, `dev` / `dev:ui` need a Muscat on `localhost:3000`:
|
|
932
|
+
|
|
933
|
+
- reuses any container already publishing `:10000` (`muscat`, `muscat-dev`, …);
|
|
934
|
+
- otherwise starts `muscat-pk-muscat` — the S3 runtime image running only `common`, cached in `~/.muscat-pk/muscat/`. It keeps running between runs.
|
|
935
|
+
|
|
936
|
+
```bash
|
|
937
|
+
docker rm -f muscat-pk-muscat # stop it
|
|
938
|
+
MUSCAT_PK_NO_LOCAL_MUSCAT=1 muscat-pk dev # don't start it
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
### 9.2 Developing on the robot PC
|
|
942
|
+
|
|
943
|
+
On the robot PC Muscat already serves the gateway on `10000`; the CLI uses it and starts no local Muscat.
|
|
944
|
+
|
|
945
|
+
### 9.3 Several developers on one robot
|
|
946
|
+
|
|
947
|
+
Against a remote robot, `dev` / `dev:back` run the plugin as `<manifest id>.dev-<user>-<random>` (random part kept in `~/.muscat-pk/dev-id`), so two machines don't answer each other's UI. UI, backend, ROS namespace and external routes all use the dev id. `--keep-id` uses the manifest id (for program `execute` or external clients) — then only one machine may connect.
|
|
948
|
+
|
|
949
|
+
### 9.4 Local vs global install
|
|
950
|
+
|
|
951
|
+
If the project has its own `muscat-plugin-kit` in `node_modules`, any `muscat-pk` hands off to it, so the CLI, `vite` preset and `preview-host` are always the project's pinned version. `muscat-pk version` prints the copy that actually ran.
|
|
952
|
+
|
|
953
|
+
### 9.5 Offline / air-gapped
|
|
954
|
+
|
|
955
|
+
Docker images are built only when missing (a rebuild contacts the registry even when cached).
|
|
956
|
+
|
|
957
|
+
```bash
|
|
958
|
+
docker save muscat-pk-python-dev muscat-pk-runtime | gzip > muscat-pk-images.tar.gz # online machine
|
|
959
|
+
gunzip -c muscat-pk-images.tar.gz | docker load # offline machine
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
Force a rebuild with `MUSCAT_PK_REBUILD_IMAGE=1`.
|
|
963
|
+
|
|
964
|
+
### 9.6 IDE support (Python)
|
|
965
|
+
|
|
966
|
+
`muscat-pk new` creates `.venv` with `requirements.txt` installed — for editor linting only; the backend runs in Docker. Without `uv` or Python 3 it prints `Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.`; install one and re-run `new` or `type change`.
|
|
967
|
+
|
|
968
|
+
### 9.7 JavaScript dependencies
|
|
969
|
+
|
|
970
|
+
`build` runs the project's own Vite build, so packages must be installed. Don't `npm install --omit=dev` before building — `vite`, `typescript`, `tailwindcss`, `sass`, `less` are devDependencies. The ZIP ships no `node_modules`.
|
|
971
|
+
|
|
972
|
+
---
|
|
973
|
+
|
|
974
|
+
## 10. Styles
|
|
975
|
+
|
|
976
|
+
- `muscat()` auto-loads `ui/style.{css,scss,sass,less}` / `ui/styles.*` for every entry and `ui/{entry}.*` for a matching entry, and injects the CSS into the JS bundle.
|
|
977
|
+
- Built selectors are scoped under `[data-muscat-plugin-id="..."]`, so plugin CSS can't touch the host or other plugins.
|
|
978
|
+
- Use design-token classes (`bg-bg-*`, `text-fg-*`, `typo-*`) rather than hex colors.
|
|
979
|
+
|
|
980
|
+
Tailwind (default, no 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` (or `less`) and create `ui/style.scss` (`ui/style.less`).
|
|
990
|
+
|
|
991
|
+
Emotion `styled` works as is. For the `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
|
+
and `"jsxImportSource": "@emotion/react"` in `tsconfig.json`. Emotion `Global` is not scoped — avoid it.
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## 11. AI coding agent setup (`muscat-pk mcp`)
|
|
1006
|
+
|
|
1007
|
+
An MCP server (stdio) with grounded info on rb-components (props, Storybook examples), design tokens, and the Python `rainbow-rb-sdk` (introspected signatures/docstrings), plus `check_project` for upgrading old projects.
|
|
1008
|
+
|
|
1009
|
+
Claude Code — `.mcp.json` at the plugin root:
|
|
1010
|
+
|
|
1011
|
+
```json
|
|
1012
|
+
{ "mcpServers": { "muscat": { "command": "muscat-pk", "args": ["mcp"] } } }
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
Codex — `muscat-pk mcp --install codex`, or `~/.codex/config.toml`:
|
|
266
1016
|
|
|
267
1017
|
```toml
|
|
268
1018
|
[mcp_servers.muscat]
|
|
@@ -270,13 +1020,40 @@ command = "muscat-pk"
|
|
|
270
1020
|
args = ["mcp"]
|
|
271
1021
|
```
|
|
272
1022
|
|
|
273
|
-
|
|
1023
|
+
The agent starts the server itself.
|
|
1024
|
+
|
|
1025
|
+
---
|
|
274
1026
|
|
|
275
|
-
##
|
|
1027
|
+
## 12. Environment variables
|
|
276
1028
|
|
|
277
|
-
|
|
|
1029
|
+
| Variable | Effect |
|
|
278
1030
|
|---|---|
|
|
279
|
-
|
|
|
280
|
-
|
|
|
1031
|
+
| `MUSCAT_ROBOT_HOST` | robot IP for `dev` / `dev:ui` / `dev:back` (skips the prompt) |
|
|
1032
|
+
| `MUSCAT_ROBOT_TOKEN` | service token when the robot's login policy is on |
|
|
1033
|
+
| `MUSCAT_PK_NO_LOCAL_MUSCAT=1` | don't start the local Muscat runtime |
|
|
1034
|
+
| `MUSCAT_PK_REBUILD_IMAGE=1` | rebuild Docker images |
|
|
1035
|
+
| `MUSCAT_PK_RB_SDK_VERSION` | C++ SDK version baked into the C++ images (with `MUSCAT_PK_REBUILD_IMAGE=1`) |
|
|
1036
|
+
| `MUSCAT_PK_NO_UPDATE_CHECK=1` | skip the update check on `dev` / `dev:ui` |
|
|
1037
|
+
| `RB_REGISTRY` | npm registry for the update check (default `https://registry.npmjs.org`) |
|
|
1038
|
+
| `MUSCAT_PK_TAG` | install script channel / version |
|
|
1039
|
+
| `NO_COLOR` / `FORCE_COLOR` | output colors |
|
|
1040
|
+
|
|
1041
|
+
Inside the backend container: `RB_PLUGIN_ID` (the plugin / dev id).
|
|
281
1042
|
|
|
282
|
-
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
## 13. Troubleshooting
|
|
1046
|
+
|
|
1047
|
+
| Symptom | Fix |
|
|
1048
|
+
|---|---|
|
|
1049
|
+
| `manifest.json errors: …` | fix each listed field (section 5) |
|
|
1050
|
+
| backend stops with an undeclared port | add the port (28600-28999) to `ports` |
|
|
1051
|
+
| UI preview says "no default export" | entry must `export default App`; `vite.config.ts` must use `muscat()`; run `check_project` via `muscat-pk mcp` |
|
|
1052
|
+
| `vite not found — UI will not load` | run `npm install` in the project |
|
|
1053
|
+
| `AttributeError: router` | robot SDK older than 0.0.9.dev5 |
|
|
1054
|
+
| step never finishes | `on_execute` must `await ctx.done()` |
|
|
1055
|
+
| Stop doesn't stop the plugin | call `await ctx.check_stop()` in every loop |
|
|
1056
|
+
| `RuntimeError` on `set_variable` | new variable names need `RBP_` |
|
|
1057
|
+
| `get_variable` returns `None` | no program running (or `IS_DEV=true` for executor state) |
|
|
1058
|
+
| external call 504 after restart | wait up to a minute for the gateway to drop the old socket |
|
|
1059
|
+
| a newer/older local install answers | expected — the project's local kit wins; `muscat-pk update` |
|