@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.3 → 0.1.2-dev.30

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.
Files changed (48) hide show
  1. package/README.md +95 -16
  2. package/cli/README.md +4 -5
  3. package/cli/bin/create-muscat-plugin.mjs +1 -1
  4. package/cli/bin/muscat-pk.mjs +16 -10
  5. package/cli/docker/Dockerfile.bridge +17 -0
  6. package/cli/docker/Dockerfile.cpp +22 -0
  7. package/cli/docker/Dockerfile.dev +14 -0
  8. package/cli/docker/Dockerfile.python +20 -0
  9. package/cli/docker/bridge-package.json +8 -0
  10. package/cli/src/bridge.mjs +92 -0
  11. package/cli/src/build.mjs +406 -36
  12. package/cli/src/check-package.mjs +2 -1
  13. package/cli/src/cpp.mjs +65 -0
  14. package/cli/src/dev.mjs +16 -3
  15. package/cli/src/devBack.mjs +246 -49
  16. package/cli/src/devUi.mjs +118 -15
  17. package/cli/src/esbuild.mjs +51 -10
  18. package/cli/src/manifest.mjs +189 -0
  19. package/cli/src/new.mjs +152 -27
  20. package/cli/src/preview.mjs +91 -0
  21. package/cli/src/python.mjs +97 -0
  22. package/cli/src/ros2.mjs +66 -0
  23. package/cli/src/socketBridge.mjs +129 -0
  24. package/cli/src/type.mjs +114 -0
  25. package/cli/src/update.mjs +267 -30
  26. package/cli/src/util.mjs +65 -4
  27. package/cli/templates/common/README.md +382 -9
  28. package/cli/templates/common/manifest.json +13 -13
  29. package/cli/templates/common/package.json +5 -11
  30. package/cli/templates/common/py/main.py +88 -0
  31. package/cli/templates/common/requirements.txt +4 -0
  32. package/cli/templates/common/tsconfig.json +1 -1
  33. package/cli/templates/common/{src → ui}/content.tsx +3 -2
  34. package/cli/templates/common/vite.config.ts +22 -13
  35. package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
  36. package/cli/templates/cpp/cpp/main.cpp +25 -0
  37. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/__init__.py +0 -0
  38. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -0
  39. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -0
  40. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/resource/__PLUGIN_SLUG__ +0 -0
  41. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -0
  42. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +17 -0
  43. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -0
  44. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +22 -0
  45. package/package.json +4 -4
  46. package/cli/src/pack.mjs +0 -58
  47. package/cli/templates/sdk/main.py +0 -21
  48. 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: (...m) => console.log(...m),
50
- step: (...m) => console.log("›", ...m),
51
- ok: (...m) => console.log("✓", ...m),
52
- warn: (...m) => console.warn("⚠", ...m),
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 scaffolded with `muscat-pk`.
3
+ A Muscat plugin project. Build, develop, and ship with `muscat-pk` CLI.
4
+
5
+ ## Quick start
4
6
 
5
7
  ```bash
6
- __PM__ run dev # preview in a mini-Muscat (preview-host) + back end
7
- __PM__ run dev:ui # UI preview only
8
- __PM__ run pack # build a distributable ZIP
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
- - `src/content.tsx` — your plugin UI. Import components from `@rainbow-robotics/plugin-ui/react`.
12
- - `manifest.json` — id, contributions, actions, runtime.
13
- - `assets/` — static assets (icons referenced from the manifest).
14
- - `main.py` — (sdk runtime) back-end handler using `rainbow-rb-sdk`.
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
- The plugin only emits `rb-*` name tags; Muscat renders the real components.
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
- "minAppVersion": "2.0.0",
6
- "icon": "assets/icon.svg",
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": "header.toolbar",
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": "^0.1.0"
7
+ "@rainbow-robotics/plugin-ui": "__RB_VERSION__"
15
8
  },
16
9
  "devDependencies": {
17
- "@rainbow-robotics/muscat-plugin-kit": "^0.1.0",
18
- "@rainbow-robotics/preview-host": "^0.1.0",
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.4.1"
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()
@@ -0,0 +1,4 @@
1
+ rainbow-rb-sdk==0.0.9.dev43
2
+ rainbow-rb-flat-buffers==0.0.9.dev43
3
+ rainbow-rb-zenoh==0.0.9.dev43
4
+ requests==2.34.2
@@ -9,5 +9,5 @@
9
9
  "noEmit": true,
10
10
  "lib": ["DOM", "DOM.Iterable", "ESNext"]
11
11
  },
12
- "include": ["src"]
12
+ "include": ["ui"]
13
13
  }
@@ -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, useRb } from "@rainbow-robotics/plugin-ui/react";
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.notify(next ? "opened" : "closed", "info");
26
+ rb.toast(next ? "opened" : "closed", "info");
26
27
  }}
27
28
  />
28
29
  </>