@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.3 → 0.1.2-dev.31
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.md +95 -16
- package/cli/README.md +4 -5
- package/cli/bin/create-muscat-plugin.mjs +1 -1
- package/cli/bin/muscat-pk.mjs +16 -10
- package/cli/docker/Dockerfile.bridge +17 -0
- package/cli/docker/Dockerfile.cpp +22 -0
- package/cli/docker/Dockerfile.dev +14 -0
- package/cli/docker/Dockerfile.python +20 -0
- package/cli/docker/bridge-package.json +8 -0
- package/cli/src/bridge.mjs +92 -0
- package/cli/src/build.mjs +406 -36
- package/cli/src/check-package.mjs +2 -1
- package/cli/src/cpp.mjs +65 -0
- package/cli/src/dev.mjs +16 -3
- package/cli/src/devBack.mjs +246 -49
- package/cli/src/devUi.mjs +118 -15
- package/cli/src/esbuild.mjs +51 -10
- package/cli/src/manifest.mjs +189 -0
- package/cli/src/new.mjs +152 -27
- package/cli/src/preview.mjs +91 -0
- package/cli/src/python.mjs +112 -0
- package/cli/src/ros2.mjs +66 -0
- package/cli/src/socketBridge.mjs +129 -0
- package/cli/src/type.mjs +114 -0
- package/cli/src/update.mjs +267 -30
- package/cli/src/util.mjs +65 -4
- package/cli/templates/common/README.md +382 -9
- package/cli/templates/common/manifest.json +13 -13
- package/cli/templates/common/package.json +5 -11
- package/cli/templates/common/py/main.py +88 -0
- package/cli/templates/common/requirements.txt +4 -0
- package/cli/templates/common/tsconfig.json +1 -1
- package/cli/templates/common/{src → ui}/content.tsx +3 -2
- package/cli/templates/common/vite.config.ts +22 -13
- package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
- package/cli/templates/cpp/cpp/main.cpp +25 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/__init__.py +0 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/resource/__PLUGIN_SLUG__ +0 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +17 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +22 -0
- package/package.json +4 -4
- package/cli/src/pack.mjs +0 -58
- package/cli/templates/sdk/main.py +0 -21
- package/cli/templates/sdk/requirements.txt +0 -1
package/cli/src/util.mjs
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
// Shared helpers for muscat-pk.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
|
|
7
|
+
/** True if an executable is on PATH (cross-platform). */
|
|
8
|
+
export function hasBin(bin) {
|
|
9
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
10
|
+
return spawnSync(probe, [bin]).status === 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find a Python 3.12 interpreter (robot targets 3.12), cross-platform.
|
|
15
|
+
* Returns the command as an argv array, e.g. ["py","-3.12"] or ["python3.12"].
|
|
16
|
+
* Falls back to a generic python3/python with a warning flag.
|
|
17
|
+
*/
|
|
18
|
+
export function detectPythonCmd() {
|
|
19
|
+
const candidates =
|
|
20
|
+
process.platform === "win32"
|
|
21
|
+
? [["py", "-3.12"], ["python3.12"], ["py", "-3"], ["python"]]
|
|
22
|
+
: [["python3.12"], ["python3"], ["python"]];
|
|
23
|
+
for (const cmd of candidates) {
|
|
24
|
+
const r = spawnSync(cmd[0], [...cmd.slice(1), "--version"]);
|
|
25
|
+
if (r.status === 0) {
|
|
26
|
+
const ver = String(r.stdout || r.stderr || "");
|
|
27
|
+
return { cmd, is312: ver.includes("3.12") };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { cmd: process.platform === "win32" ? ["python"] : ["python3"], is312: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Path to the venv's python interpreter (cross-platform). */
|
|
34
|
+
export function venvPython(venvDir) {
|
|
35
|
+
return process.platform === "win32"
|
|
36
|
+
? resolve(venvDir, "Scripts", "python.exe")
|
|
37
|
+
: resolve(venvDir, "bin", "python");
|
|
38
|
+
}
|
|
39
|
+
|
|
6
40
|
/** Published install/CLI package name. Author imports use @rainbow-robotics/plugin-ui separately. */
|
|
7
41
|
export const KIT_PUBLISHED_NAME = "@rainbow-robotics/muscat-plugin-kit";
|
|
8
42
|
|
|
@@ -45,13 +79,40 @@ export function findPluginRoot(start = process.cwd()) {
|
|
|
45
79
|
);
|
|
46
80
|
}
|
|
47
81
|
|
|
82
|
+
const RED = "\x1b[31m";
|
|
83
|
+
const ORANGE = "\x1b[33m";
|
|
84
|
+
const RESET = "\x1b[0m";
|
|
85
|
+
|
|
48
86
|
export const log = {
|
|
49
|
-
info:
|
|
50
|
-
step:
|
|
51
|
-
ok:
|
|
52
|
-
warn:
|
|
87
|
+
info: (...m) => console.log(...m),
|
|
88
|
+
step: (...m) => console.log("›", ...m),
|
|
89
|
+
ok: (...m) => console.log("✓", ...m),
|
|
90
|
+
warn: (...m) => console.warn(`${ORANGE}⚠${RESET}`, ...m.map(s => `${ORANGE}${s}${RESET}`)),
|
|
91
|
+
error: (...m) => console.error(`${RED}✖${RESET}`, ...m.map(s => `${RED}${s}${RESET}`)),
|
|
53
92
|
};
|
|
54
93
|
|
|
94
|
+
// dev / dev:back / build run the plugin back end, the dev bridge, and cpp/ros2
|
|
95
|
+
// compilation in Docker. Show a clear install hint when Docker is missing.
|
|
96
|
+
export function ensureDocker() {
|
|
97
|
+
if (hasBin("docker")) return;
|
|
98
|
+
log.warn("Docker not found.");
|
|
99
|
+
log.warn(" muscat-pk runs the plugin back end, the dev bridge, and cpp/ros2 builds in Docker.");
|
|
100
|
+
log.warn(" Install Docker, then retry: https://docs.docker.com/get-docker/");
|
|
101
|
+
throw new Error("Docker is required for dev / dev:back / build.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Remove dangling (untagged) muscat-pk images left behind after a rebuild.
|
|
106
|
+
* Uses label filter so only our images are touched — other Docker images are safe.
|
|
107
|
+
*/
|
|
108
|
+
export function pruneMuskatImages() {
|
|
109
|
+
spawnSync(
|
|
110
|
+
"docker",
|
|
111
|
+
["image", "prune", "--force", "--filter", "label=com.rainbow-robotics.muscat-pk=true"],
|
|
112
|
+
{ stdio: "ignore" },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
55
116
|
export function humanSize(bytes) {
|
|
56
117
|
if (bytes < 1024) return `${bytes} B`;
|
|
57
118
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -1,16 +1,389 @@
|
|
|
1
1
|
# __PLUGIN_NAME__
|
|
2
2
|
|
|
3
|
-
A Muscat plugin
|
|
3
|
+
A Muscat plugin project. Build, develop, and ship with `muscat-pk` CLI.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
muscat-pk dev # UI preview + backend together
|
|
9
|
+
muscat-pk dev:ui # UI preview only
|
|
10
|
+
muscat-pk dev:back # backend only (Python watchfiles / ROS2 Docker)
|
|
11
|
+
muscat-pk build # build + distributable ZIP
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Project structure
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
__PLUGIN_DIR__/
|
|
20
|
+
├─ ui/
|
|
21
|
+
│ └─ content.tsx # plugin UI entry point
|
|
22
|
+
├─ py/
|
|
23
|
+
│ └─ main.py # Python backend entry point
|
|
24
|
+
├─ assets/
|
|
25
|
+
│ └─ icon.svg # icon referenced from manifest
|
|
26
|
+
├─ manifest.json # metadata, contribution points, runtime config
|
|
27
|
+
├─ README.md # this file (copied to dist/ on build)
|
|
28
|
+
└─ requirements.txt # Python deps (auto-pinned to latest on build)
|
|
9
29
|
```
|
|
10
30
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
> ROS2 plugins additionally include a `ros2_ws/` directory.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Frontend (`ui/content.tsx`)
|
|
36
|
+
|
|
37
|
+
### useRb() hook
|
|
38
|
+
|
|
39
|
+
Access the communication API between the plugin module and UI from any plugin component.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
|
|
43
|
+
|
|
44
|
+
function App() {
|
|
45
|
+
const rb = useRb();
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### rb API
|
|
50
|
+
|
|
51
|
+
| Method | Description |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `rb.pub(topic, payload)` | Publish a message to a topic |
|
|
54
|
+
| `rb.sub(topic, handler)` | Subscribe to a topic — returns `{ undeclare() }` |
|
|
55
|
+
| `rb.action(topic, payload?)` | Send a request and await a reply |
|
|
56
|
+
| `rb.serve(topic, handler)` | Handle incoming requests from the backend |
|
|
57
|
+
| `rb.toast(message, type?)` | Host toast notification (`"info" · "success" · "warning" · "error"`) |
|
|
58
|
+
|
|
59
|
+
All methods support Generic Types:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
const status = await rb.action<{ ok: boolean }, { id: number }>(
|
|
63
|
+
"get-status",
|
|
64
|
+
{ id: 1 },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const sub = await rb.sub<{ value: number }>("sensor/data", (data) => {
|
|
68
|
+
console.log(data.value);
|
|
69
|
+
});
|
|
70
|
+
sub.undeclare(); // cleanup
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Topic namespace
|
|
74
|
+
|
|
75
|
+
Topics are automatically namespaced by plugin ID to prevent collisions across plugins.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
"sensor/data" → plugin/{pluginId}/sensor/data (auto-namespaced)
|
|
79
|
+
"/broadcast/x" → broadcast/x (leading / = absolute)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### UI components
|
|
83
|
+
|
|
84
|
+
Import Muscat components from `@rainbow-robotics/plugin-ui/react`. The plugin emits
|
|
85
|
+
`rb-*` tags; the host renders the actual components.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { connect } from "@rainbow-robotics/plugin-ui/connect";
|
|
89
|
+
import { Button, Slider, Text, Switch } from "@rainbow-robotics/plugin-ui/react";
|
|
90
|
+
import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
|
|
91
|
+
|
|
92
|
+
function App() {
|
|
93
|
+
const rb = useRb();
|
|
94
|
+
return <Button label="Run" onPress={() => rb.action("run")} />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
connect(App);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Backend (`py/main.py`)
|
|
103
|
+
|
|
104
|
+
The backend communicates with the frontend using `rainbow.rb_sdk.RBPluginCall`.
|
|
105
|
+
`muscat-pk dev` starts the Socket.IO bridge in Docker (published on port 10000) and
|
|
106
|
+
runs the backend container sharing its network, so the SDK connects at localhost:10000.
|
|
107
|
+
`py/main.py` should contain plugin logic only and must not start its own server.
|
|
108
|
+
|
|
109
|
+
### Python plugin
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import time
|
|
113
|
+
from rainbow.rb_sdk import RBAmrSDK, RBManipulateSDK, RBPluginCall
|
|
114
|
+
|
|
115
|
+
plugin = RBPluginCall()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
plugin.publish("state", {"ready": True})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@plugin.subscribe("event")
|
|
122
|
+
def on_ui_event(payload=None):
|
|
123
|
+
print(payload)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@plugin.serve("tool/echo")
|
|
127
|
+
def echo(payload=None):
|
|
128
|
+
return {"echo": payload}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def call_frontend():
|
|
132
|
+
res = plugin.call("tool/ping", payload={"hello": "plugin"})
|
|
133
|
+
print(res)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def move_j_example():
|
|
137
|
+
amr = RBAmrSDK()
|
|
138
|
+
manipulate = RBManipulateSDK()
|
|
139
|
+
return manipulate.move.call_move_j(
|
|
140
|
+
robot_model="C500920",
|
|
141
|
+
target={"tar_values": [0, 0, 0, 0, 0, 0], "tar_frame": 0, "tar_unit": 0},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
while True:
|
|
147
|
+
time.sleep(1)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### ROS2 plugin
|
|
151
|
+
|
|
152
|
+
The node is launched with `--ros-args -r __ns:=/{slug}` automatically by `dev:back`.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py
|
|
156
|
+
import rclpy
|
|
157
|
+
from rclpy.node import Node
|
|
158
|
+
from std_msgs.msg import Float32
|
|
159
|
+
|
|
160
|
+
class __PLUGIN_CLASS__Node(Node):
|
|
161
|
+
def __init__(self):
|
|
162
|
+
super().__init__("__PLUGIN_SLUG___node")
|
|
163
|
+
# relative topic name — __ns prefix applied automatically
|
|
164
|
+
self.pub = self.create_publisher(Float32, "sensor_data", 10)
|
|
165
|
+
self.create_timer(1.0, self.timer_cb)
|
|
166
|
+
|
|
167
|
+
def timer_cb(self):
|
|
168
|
+
self.pub.publish(Float32(data=42.0))
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## manifest.json
|
|
15
174
|
|
|
16
|
-
|
|
175
|
+
```jsonc
|
|
176
|
+
{
|
|
177
|
+
// unique plugin ID — reverse-domain format recommended
|
|
178
|
+
"id": "com.example.my-plugin",
|
|
179
|
+
"name": "My Plugin",
|
|
180
|
+
"version": "1.0.0",
|
|
181
|
+
|
|
182
|
+
// minimum Muscat version required to load this plugin
|
|
183
|
+
"minMuscatVersion": "1.0.0",
|
|
184
|
+
|
|
185
|
+
// plugin author (empty string is fine)
|
|
186
|
+
"author": "Your Name",
|
|
187
|
+
|
|
188
|
+
// displayed in the Muscat plugin store / info panel
|
|
189
|
+
// string or { en (required), ko, ja, zh, vi, th, es }
|
|
190
|
+
"description": { "en": "Plugin description", "ko": "플러그인 설명" },
|
|
191
|
+
|
|
192
|
+
// SVG string (single file) or PNG/JPG size map (all three sizes required)
|
|
193
|
+
"icons": "assets/icon.svg",
|
|
194
|
+
// "icons": { "16": "assets/icon-16.png", "48": "assets/icon-48.png", "128": "assets/icon-128.png" },
|
|
195
|
+
|
|
196
|
+
// targetModels and/or targetCategorys — at least one is required
|
|
197
|
+
// "all" = every target, or list specific values
|
|
198
|
+
"targetModels": "all",
|
|
199
|
+
// "targetModels": ["C500920", "C500930"],
|
|
200
|
+
"targetCategorys": "all",
|
|
201
|
+
// "targetCategorys": ["manipulate", "amr", "rby1", "vision"],
|
|
202
|
+
|
|
203
|
+
"contributes": [
|
|
204
|
+
// program.command — adds a step node to the program tree
|
|
205
|
+
{
|
|
206
|
+
"point": "program.command",
|
|
207
|
+
"icon": "assets/icon.svg",
|
|
208
|
+
"label": { "ko": "내 플러그인", "en": "My Plugin" },
|
|
209
|
+
"ui": "program/content", // must be unique across all contributions
|
|
210
|
+
"commandInfo": {
|
|
211
|
+
"name": "My Plugin",
|
|
212
|
+
"icon": "assets/icon.svg",
|
|
213
|
+
"funcName": "plugin.run",
|
|
214
|
+
"args": {}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
// toolbar — adds a button to the top toolbar (required)
|
|
218
|
+
{
|
|
219
|
+
"point": "toolbar",
|
|
220
|
+
"icon": "assets/icon.svg",
|
|
221
|
+
"label": { "ko": "내 플러그인", "en": "My Plugin" },
|
|
222
|
+
"ui": "content", // must be unique across all contributions
|
|
223
|
+
"opens": "popover"
|
|
224
|
+
},
|
|
225
|
+
// move.rnb — adds a panel to the Move screen right-side area
|
|
226
|
+
{
|
|
227
|
+
"point": "move.rnb",
|
|
228
|
+
"icon": "assets/icon.svg",
|
|
229
|
+
"label": { "ko": "내 플러그인", "en": "My Plugin" },
|
|
230
|
+
"ui": "move/content" // must be unique across all contributions
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
|
|
234
|
+
// runtime — one of:
|
|
235
|
+
// { "type": "none" }
|
|
236
|
+
// { "type": "python", "entry": "main.py" }
|
|
237
|
+
// { "type": "cpp", "entry": "<executable name>" }
|
|
238
|
+
// { "type": "ros2", "rosClient": "rclpy" | "rclcpp", "package": "...", "launch": "..._node" }
|
|
239
|
+
"runtime": { "type": "python", "entry": "main.py" }
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Backend types:**
|
|
244
|
+
|
|
245
|
+
| type | source dir | build / run | entry |
|
|
246
|
+
|---|---|---|---|
|
|
247
|
+
| `python` | `py/` | pip + run in Docker | `main.py` |
|
|
248
|
+
| `cpp` | `cpp/` | CMake in Docker | built executable name |
|
|
249
|
+
| `ros2` (rclpy) | `ros2_ws/` | colcon (ament_python) in Docker | — |
|
|
250
|
+
| `ros2` (rclcpp) | `ros2_ws/` | colcon (ament_cmake) in Docker | — |
|
|
251
|
+
|
|
252
|
+
> Every back end runs in a Docker container matching the robot (Ubuntu 24.04 + Python 3.12), so dev behaves like production and compiled binaries are ABI-compatible. **Docker is required** for all backend types (only a frontend-only `none` plugin skips it). No local Python/toolchain setup is needed.
|
|
253
|
+
|
|
254
|
+
**Contribution points:**
|
|
255
|
+
|
|
256
|
+
| point | Where it appears | `commandInfo` required |
|
|
257
|
+
|---|---|:---:|
|
|
258
|
+
| `toolbar` | Top toolbar button (must have at least one) | — |
|
|
259
|
+
| `program.command` | Program tree step node | ✓ |
|
|
260
|
+
| `move.rnb` | Move screen right-side panel | — |
|
|
261
|
+
|
|
262
|
+
**`commandInfo` fields** (for `program.command`):
|
|
263
|
+
|
|
264
|
+
| field | type | description |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| `name` | `string` | Display name of the step node |
|
|
267
|
+
| `icon` | `string` | Icon path |
|
|
268
|
+
| `iconClassName` | `string?` | Optional CSS class for the icon |
|
|
269
|
+
| `funcName` | `string?` | Backend function name to call |
|
|
270
|
+
| `args` | `Record<string, any>?` | Default arguments |
|
|
271
|
+
| `disabled` | `boolean?` | Whether the node is disabled |
|
|
272
|
+
| `allowChildren` | `string[]?` | Allowed child commands |
|
|
273
|
+
| `allowParents` | `string[]?` | Allowed parent commands |
|
|
274
|
+
| `notAllowChildren` | `string[]?` | Disallowed child commands |
|
|
275
|
+
| `syncGroupId` | `boolean?` | Sync group ID across children |
|
|
276
|
+
| `disableCopy` | `boolean?` | Prevent copy |
|
|
277
|
+
| `disableDelete` | `boolean?` | Prevent delete |
|
|
278
|
+
| `requireChildren` | `boolean?` | Whether a child command is required |
|
|
279
|
+
| `requireParent` | `boolean?` | Whether a parent command is required |
|
|
280
|
+
| `childrenCommand` | `string?` | Default command type for children |
|
|
281
|
+
|
|
282
|
+
**Validation rules:**
|
|
283
|
+
- `id`, `name`, `version`, `minMuscatVersion` — required non-empty strings
|
|
284
|
+
- `author` — required non-empty string
|
|
285
|
+
- `description` — required (`string` or `{ en (required), ko?, ja?, zh?, vi?, th?, es? }`)
|
|
286
|
+
- `icons` — SVG: single string path ending in `.svg`; PNG/JPG: object with `"16"`, `"48"`, `"128"` all present
|
|
287
|
+
- `targetModels` — `"all"` or a non-empty array of robot model ID strings (at least one of `targetModels` / `targetCategorys` required)
|
|
288
|
+
- `targetCategorys` — `"all"` or a non-empty array of `"manipulate" | "amr" | "rby1" | "vision"` (at least one of `targetModels` / `targetCategorys` required)
|
|
289
|
+
- `contributes` — required array; must contain at least one `toolbar` entry
|
|
290
|
+
- `ui` — each contribution must have a unique value
|
|
291
|
+
- `commandInfo` — required when `point` is `"program.command"`
|
|
292
|
+
|
|
293
|
+
`muscat-pk build` throws on any validation error. `muscat-pk dev` prints warnings.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Localized documentation
|
|
298
|
+
|
|
299
|
+
You can ship per-language README files alongside the main `README.md`. Place them in the project root:
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
README.md # default / English fallback
|
|
303
|
+
README.ko.md # Korean
|
|
304
|
+
README.en.md # explicit English (optional)
|
|
305
|
+
README.ja.md # Japanese
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
All `README*.md` files found at the project root are automatically copied to `dist/` on
|
|
309
|
+
`muscat-pk build`. Muscat displays the file matching the user's locale,
|
|
310
|
+
falling back to `README.md` if none matches.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Installing Python dependencies
|
|
315
|
+
|
|
316
|
+
Add any package with an optional version pin to `requirements.txt`:
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
rainbow-rb-sdk
|
|
320
|
+
rainbow-rb-flat-buffers
|
|
321
|
+
numpy==1.26.4
|
|
322
|
+
opencv-python
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed automatically inside the dev container (`pip install -r requirements.txt`) on every startup. No local Python or venv needed.
|
|
326
|
+
|
|
327
|
+
> `muscat-pk build` only **pins** versions in `requirements.txt` — it does not run `pip install` (the robot installs them at load time).
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Switching backend type
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
muscat-pk type change # interactive
|
|
335
|
+
muscat-pk type change --to cpp # → C++
|
|
336
|
+
muscat-pk type change --to ros2 # → ROS2 (rclpy / Python)
|
|
337
|
+
muscat-pk type change --to ros2-cpp # → ROS2 (rclcpp / C++)
|
|
338
|
+
muscat-pk type change --to python # → Python
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
> Existing backend dirs (`ros2_ws/`, `cpp/`) are not deleted when switching away. Remove them manually if no longer needed.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Shipping
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
muscat-pk build # build + ZIP
|
|
349
|
+
muscat-pk build --no-zip # build only, no ZIP
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
- Builds `ui/content.tsx` → `dist/content.js` (minified)
|
|
353
|
+
- Pins `requirements.txt` to latest versions (stable-only on `main` branch)
|
|
354
|
+
- Copies `assets/`, `manifest.json`, `README*.md`, `py/`, and `requirements.txt` into `dist/`
|
|
355
|
+
- ROS2: runs `colcon build` inside the Jazzy container; includes `ros2_ws/install/`
|
|
356
|
+
- Output: `__PLUGIN_DIR__-{version}.zip`
|
|
357
|
+
|
|
358
|
+
ZIP layout:
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
__PLUGIN_DIR__-1.0.0.zip
|
|
362
|
+
├─ dist/
|
|
363
|
+
│ ├─ content.js # built UI
|
|
364
|
+
│ ├─ manifest.json
|
|
365
|
+
│ ├─ assets/
|
|
366
|
+
│ ├─ README.md
|
|
367
|
+
│ ├─ README.ko.md # if present
|
|
368
|
+
│ ├─ main.py # from py/
|
|
369
|
+
│ └─ requirements.txt # pinned
|
|
370
|
+
└─ ros2_ws/install/ # ROS2 only
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## All commands
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
muscat-pk dev # UI + backend
|
|
379
|
+
muscat-pk dev:ui # UI preview only (preview-host)
|
|
380
|
+
muscat-pk dev:back # backend only (Python: watchfiles / ROS2: Docker)
|
|
381
|
+
muscat-pk build # build → dist/ + distributable ZIP (--no-zip to skip)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
muscat-pk type change # switch backend type
|
|
386
|
+
muscat-pk sdk list # show current Python SDK version
|
|
387
|
+
muscat-pk sdk update # update Python SDK to latest
|
|
388
|
+
muscat-pk update # update muscat-plugin-kit itself
|
|
389
|
+
```
|
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
"id": "__PLUGIN_ID__",
|
|
3
3
|
"name": "__PLUGIN_NAME__",
|
|
4
4
|
"version": "1.0.0",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"minMuscatVersion": "1.0.0",
|
|
6
|
+
"author": "__PLUGIN_AUTHOR__",
|
|
7
|
+
"email": "__PLUGIN_EMAIL__",
|
|
8
|
+
"description": {
|
|
9
|
+
"ko": "",
|
|
10
|
+
"en": ""
|
|
11
|
+
},
|
|
12
|
+
"icons": "assets/icon.svg",
|
|
13
|
+
"targetModels": "all",
|
|
14
|
+
"targetCategorys": "all",
|
|
7
15
|
"contributes": [
|
|
8
|
-
{ "point": "program.panel", "label": "__PLUGIN_NAME__", "icon": "assets/icon.svg" },
|
|
9
|
-
{ "point": "lnb", "label": "__PLUGIN_NAME__", "icon": "assets/icon.svg" },
|
|
10
16
|
{
|
|
11
|
-
"point": "
|
|
12
|
-
"label": "__PLUGIN_NAME__",
|
|
17
|
+
"point": "toolbar",
|
|
13
18
|
"icon": "assets/icon.svg",
|
|
19
|
+
"label": { "ko": "__PLUGIN_NAME__", "en": "__PLUGIN_NAME__" },
|
|
20
|
+
"ui": "content",
|
|
14
21
|
"opens": "popover"
|
|
15
22
|
}
|
|
16
23
|
],
|
|
17
|
-
"actions": [
|
|
18
|
-
{
|
|
19
|
-
"id": "grip",
|
|
20
|
-
"topic": "rby2/plugin/__PLUGIN_ID__/grip",
|
|
21
|
-
"schema": { "width": "number" }
|
|
22
|
-
}
|
|
23
|
-
],
|
|
24
24
|
"runtime": { "type": "__RUNTIME_TYPE__", "entry": "main.py" }
|
|
25
25
|
}
|
|
@@ -3,20 +3,14 @@
|
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "muscat-pk dev",
|
|
8
|
-
"dev:ui": "muscat-pk dev:ui",
|
|
9
|
-
"dev:back": "muscat-pk dev:back",
|
|
10
|
-
"build": "muscat-pk build",
|
|
11
|
-
"pack": "muscat-pk pack"
|
|
12
|
-
},
|
|
13
6
|
"dependencies": {
|
|
14
|
-
"@rainbow-robotics/plugin-ui": "
|
|
7
|
+
"@rainbow-robotics/plugin-ui": "__RB_VERSION__"
|
|
15
8
|
},
|
|
16
9
|
"devDependencies": {
|
|
17
|
-
"@rainbow-robotics/muscat-plugin-kit": "
|
|
18
|
-
"@rainbow-robotics/preview-host": "
|
|
10
|
+
"@rainbow-robotics/muscat-plugin-kit": "__RB_VERSION__",
|
|
11
|
+
"@rainbow-robotics/preview-host": "__RB_VERSION__",
|
|
12
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
19
13
|
"typescript": "^5.8.0",
|
|
20
|
-
"vite": "^6.
|
|
14
|
+
"vite": "^6.0.0"
|
|
21
15
|
}
|
|
22
16
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Muscat plugin backend — __PLUGIN_NAME__
|
|
3
|
+
|
|
4
|
+
Run with:
|
|
5
|
+
muscat-pk dev:back
|
|
6
|
+
python main.py
|
|
7
|
+
|
|
8
|
+
This template uses the high-level Rainbow Python SDK instead of wiring the
|
|
9
|
+
transport layer directly.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from rainbow.rb_sdk import RBManipulateSDK, RBPluginCall
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
PLUGIN_NAME = "__PLUGIN_NAME__"
|
|
19
|
+
|
|
20
|
+
plugin = RBPluginCall()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@plugin.subscribe("event")
|
|
24
|
+
def on_ui_event(payload: dict[str, Any] | None = None):
|
|
25
|
+
"""Handle messages published from the plugin UI with rb.pub("event", ...)."""
|
|
26
|
+
print(f"[{PLUGIN_NAME}] event:", payload)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@plugin.serve("grip")
|
|
30
|
+
def grip(payload: dict[str, Any] | None = None):
|
|
31
|
+
"""Handle the default UI button: rb.action("grip", { width })."""
|
|
32
|
+
width = (payload or {}).get("width", 20)
|
|
33
|
+
print(f"[{PLUGIN_NAME}] grip requested: width={width}")
|
|
34
|
+
|
|
35
|
+
plugin.publish("state", {"ready": True, "last_grip_width": width})
|
|
36
|
+
return {"ok": True, "width": width}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@plugin.serve("tool/echo")
|
|
40
|
+
def echo(payload: dict[str, Any] | None = None):
|
|
41
|
+
"""Small query example callable from the UI with rb.action("tool/echo", ...)."""
|
|
42
|
+
return {"echo": payload, "plugin": PLUGIN_NAME}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ping_ui_example():
|
|
46
|
+
"""
|
|
47
|
+
Example: call a queryable served by the UI.
|
|
48
|
+
|
|
49
|
+
In the frontend, register:
|
|
50
|
+
rb.serve("tool/ping", async (payload) => ({ pong: payload }))
|
|
51
|
+
"""
|
|
52
|
+
res = plugin.call("tool/ping", payload={"hello": "plugin"})
|
|
53
|
+
print(f"[{PLUGIN_NAME}] ping response:", res)
|
|
54
|
+
return res
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def move_j_example():
|
|
58
|
+
"""
|
|
59
|
+
Example: use other robot SDKs from the same backend.
|
|
60
|
+
|
|
61
|
+
This function is not called automatically. Add safety checks and call it
|
|
62
|
+
from your own handler when the robot is ready.
|
|
63
|
+
"""
|
|
64
|
+
manipulate = RBManipulateSDK()
|
|
65
|
+
|
|
66
|
+
return manipulate.move.call_move_j(
|
|
67
|
+
robot_model="C500920",
|
|
68
|
+
target={
|
|
69
|
+
"tar_values": [0, 0, 0, 0, 0, 0],
|
|
70
|
+
"tar_frame": 0,
|
|
71
|
+
"tar_unit": 0,
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
plugin.publish("state", {"ready": True, "plugin": PLUGIN_NAME})
|
|
78
|
+
print(f"[{PLUGIN_NAME}] backend ready")
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
while True:
|
|
82
|
+
time.sleep(1)
|
|
83
|
+
except KeyboardInterrupt:
|
|
84
|
+
print(f"[{PLUGIN_NAME}] stopped")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { connect } from "@rainbow-robotics/plugin-ui/connect";
|
|
3
|
-
import { Slider, Button, Text, Switch
|
|
3
|
+
import { Slider, Button, Text, Switch } from "@rainbow-robotics/plugin-ui/react";
|
|
4
|
+
import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
|
|
4
5
|
|
|
5
6
|
function App() {
|
|
6
7
|
const rb = useRb();
|
|
@@ -22,7 +23,7 @@ function App() {
|
|
|
22
23
|
onChange={(event: { detail?: boolean }) => {
|
|
23
24
|
const next = event.detail ?? false;
|
|
24
25
|
setOpen(next);
|
|
25
|
-
rb.
|
|
26
|
+
rb.toast(next ? "opened" : "closed", "info");
|
|
26
27
|
}}
|
|
27
28
|
/>
|
|
28
29
|
</>
|