@rainbow-robotics/muscat-plugin-kit 0.1.1 → 0.1.2-dev.11

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.
@@ -1,16 +1,261 @@
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
6
+
7
+ ```bash
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 pack # build a distributable ZIP
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Project structure
17
+
18
+ ```
19
+ __PLUGIN_DIR__/
20
+ ├─ src/
21
+ │ └─ content.tsx # plugin UI entry point
22
+ ├─ assets/
23
+ │ └─ icon.svg # icon referenced from manifest
24
+ ├─ manifest.json # metadata, contribution points, runtime config
25
+ ├─ main.py # backend (Python or ROS2)
26
+ └─ requirements.txt # Python deps (auto-pinned to latest on pack)
27
+ ```
28
+
29
+ > ROS2 plugins additionally include a `ros2_ws/` directory.
30
+
31
+ ---
32
+
33
+ ## Frontend (`src/content.tsx`)
34
+
35
+ ### useRb() hook
36
+
37
+ Access the communication API between the plugin module and UI from any plugin component.
38
+
39
+ ```tsx
40
+ import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
41
+
42
+ function App() {
43
+ const rb = useRb();
44
+ }
45
+ ```
46
+
47
+ ### rb API
48
+
49
+ | Method | Description |
50
+ |---|---|
51
+ | `rb.pub(topic, payload)` | Publish a message to a topic |
52
+ | `rb.sub(topic, handler)` | Subscribe to a topic — returns `{ undeclare() }` |
53
+ | `rb.action(topic, payload?)` | Send a request and await a reply |
54
+ | `rb.serve(topic, handler)` | Handle incoming requests from the backend |
55
+ | `rb.toast(message, type?)` | Host toast notification (`"info" · "success" · "warning" · "error"`) |
56
+
57
+ All methods support Generic Types:
58
+
59
+ ```tsx
60
+ const status = await rb.action<{ ok: boolean }, { id: number }>(
61
+ "get-status",
62
+ { id: 1 },
63
+ );
64
+
65
+ const sub = await rb.sub<{ value: number }>("sensor/data", (data) => {
66
+ console.log(data.value);
67
+ });
68
+ sub.undeclare(); // cleanup
69
+ ```
70
+
71
+ ### Topic namespace
72
+
73
+ Topics are automatically namespaced by plugin ID to prevent collisions across plugins.
74
+
75
+ ```
76
+ "sensor/data" → plugin/{pluginId}/sensor/data (auto-namespaced)
77
+ "/broadcast/x" → broadcast/x (leading / = absolute)
78
+ ```
79
+
80
+ ### UI components
81
+
82
+ Import Muscat components from `@rainbow-robotics/plugin-ui/react`. The plugin emits
83
+ `rb-*` tags; the host renders the actual components.
84
+
85
+ ```tsx
86
+ import { connect } from "@rainbow-robotics/plugin-ui/connect";
87
+ import { Button, Slider, Text, Switch } from "@rainbow-robotics/plugin-ui/react";
88
+ import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
89
+
90
+ function App() {
91
+ const rb = useRb();
92
+ return <Button label="Run" onPress={() => rb.action("run")} />;
93
+ }
94
+
95
+ connect(App);
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Backend (`main.py`)
101
+
102
+ The backend communicates with the frontend using `rainbow_rb_zenoh`.
103
+ Set the `prefix` to `plugin/{manifest.id}` to match the frontend's auto-namespace.
104
+
105
+ ### Python plugin
106
+
107
+ ```python
108
+ import asyncio
109
+ from rainbow_rb_zenoh import ZenohRouter, ZenohClient
110
+
111
+ router = ZenohRouter(prefix="plugin/__PLUGIN_ID__")
112
+ client = ZenohClient()
113
+
114
+
115
+ @router.subscribe("sensor/data")
116
+ async def on_sensor(*, dict_payload, **_):
117
+ print("received:", dict_payload)
118
+
119
+
120
+ @router.queryable("get-status")
121
+ async def get_status(req=None, **_):
122
+ return {"ok": True}
123
+
124
+
125
+ async def main():
126
+ await router.startup()
127
+ await asyncio.Event().wait() # keep alive
128
+
129
+
130
+ if __name__ == "__main__":
131
+ asyncio.run(main())
132
+ ```
133
+
134
+ ### ROS2 plugin
135
+
136
+ The node is launched with `--ros-args -r __ns:=/{slug}` automatically by `dev:back`.
137
+ ROS2 topics are bridged to the plugin communication layer — no extra code needed.
138
+
139
+ ```python
140
+ # ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py
141
+ import rclpy
142
+ from rclpy.node import Node
143
+ from std_msgs.msg import Float32
144
+
145
+ class __PLUGIN_CLASS__Node(Node):
146
+ def __init__(self):
147
+ super().__init__("__PLUGIN_SLUG___node")
148
+ # relative topic name — __ns prefix applied automatically
149
+ self.pub = self.create_publisher(Float32, "sensor_data", 10)
150
+ self.create_timer(1.0, self.timer_cb)
151
+
152
+ def timer_cb(self):
153
+ self.pub.publish(Float32(data=42.0))
154
+ ```
155
+
156
+ ---
157
+
158
+ ## manifest.json
159
+
160
+ ```jsonc
161
+ {
162
+ "id": "com.example.my-plugin", // unique plugin ID (reverse-domain recommended)
163
+ "name": "My Plugin",
164
+ "version": "1.0.0",
165
+ "minAppVersion": "2.0.0",
166
+ "icon": "assets/icon.svg",
167
+
168
+ "contributes": [
169
+ { "point": "program.panel", "label": "My Plugin", "icon": "assets/icon.svg" },
170
+ { "point": "lnb", "label": "My Plugin", "icon": "assets/icon.svg" },
171
+ { "point": "header.toolbar","label": "My Plugin", "icon": "assets/icon.svg", "opens": "popover" }
172
+ ],
173
+
174
+ // "python" | "ros2"
175
+ "runtime": { "type": "python", "entry": "main.py" }
176
+ }
177
+ ```
178
+
179
+ **Contribution points:**
180
+
181
+ | point | Where it appears |
182
+ |---|---|
183
+ | `program.panel` | Program panel tab |
184
+ | `lnb` | Left navigation bar |
185
+ | `header.toolbar` | Header toolbar button |
186
+
187
+ ---
188
+
189
+ ## Installing Python dependencies
190
+
191
+ Add any package with an optional version pin to `requirements.txt`:
192
+
193
+ ```
194
+ rainbow-rb-sdk
195
+ rainbow-rb-flat-buffers
196
+ numpy==1.26.4
197
+ opencv-python
198
+ ```
199
+
200
+ Then just run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed automatically on every startup:
201
+
202
+ - **Python plugin**: installed into `.venv` via `pip install -r requirements.txt`
203
+ - **ROS2 plugin**: installed inside the Docker container via `pip install -r requirements.txt`
204
+
205
+ No extra steps needed.
206
+
207
+ > `muscat-pk build` / `muscat-pk pack` only **pins** versions in `requirements.txt` — it does not run `pip install`.
208
+
209
+ ---
210
+
211
+ ## Switching backend type
212
+
213
+ ```bash
214
+ muscat-pk type change # interactive
215
+ muscat-pk type change --to ros2 # python → ros2
216
+ muscat-pk type change --to python # ros2 → python
217
+ ```
218
+
219
+ > `ros2_ws/` is not deleted when switching back to python. Remove it manually if no longer needed.
220
+
221
+ ---
222
+
223
+ ## Shipping
4
224
 
5
225
  ```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
226
+ muscat-pk pack
9
227
  ```
10
228
 
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`.
229
+ - Builds `src/content.tsx` → `dist/content.js`
230
+ - Pins `requirements.txt` to latest versions (stable-only on `main` branch)
231
+ - ROS2: runs `colcon build` inside the Jazzy container; includes `ros2_ws/install/`
232
+ - Output: `__PLUGIN_DIR__-{version}.zip`
15
233
 
16
- The plugin only emits `rb-*` name tags; Muscat renders the real components.
234
+ ZIP layout:
235
+
236
+ ```
237
+ __PLUGIN_DIR__-1.0.0.zip
238
+ ├─ dist/ # built UI
239
+ ├─ main.py
240
+ ├─ requirements.txt # pinned
241
+ └─ ros2_ws/install/ # ROS2 only
242
+ ```
243
+
244
+ ---
245
+
246
+ ## All commands
247
+
248
+ ```bash
249
+ muscat-pk dev # UI + backend
250
+ muscat-pk dev:ui # UI preview only (preview-host)
251
+ muscat-pk dev:back # backend only (Python: watchfiles / ROS2: Docker)
252
+ muscat-pk build # UI build only → dist/
253
+ muscat-pk pack # build + ZIP
254
+ ```
255
+
256
+ ```bash
257
+ muscat-pk type change # switch backend type
258
+ muscat-pk sdk list # show current Python SDK version
259
+ muscat-pk sdk update # update Python SDK to latest
260
+ muscat-pk update # update muscat-plugin-kit itself
261
+ ```
@@ -0,0 +1,114 @@
1
+ """
2
+ Muscat plugin backend — __PLUGIN_NAME__
3
+
4
+ 플러그인 UI(브라우저)와 Socket.IO로 통신한다.
5
+ `muscat-pk dev:back` 또는 `python main.py`로 실행.
6
+
7
+ 토픽 자동 네임스페이스:
8
+ "sensor/data" → plugin/__PLUGIN_ID__/sensor/data (자동)
9
+ "/broadcast/x" → broadcast/x (앞 / = 절대경로)
10
+ """
11
+
12
+ import asyncio
13
+ import socketio
14
+ from aiohttp import web
15
+
16
+ sio = socketio.AsyncServer(async_mode="aiohttp", cors_allowed_origins="*")
17
+ app = web.Application()
18
+ sio.attach(app)
19
+
20
+ PLUGIN_ID = "__PLUGIN_ID__"
21
+ _PREFIX = f"plugin/{PLUGIN_ID}"
22
+
23
+
24
+ def _topic(relative: str) -> str:
25
+ """상대 토픽 → 전체 네임스페이스 토픽."""
26
+ return relative if relative.startswith("/") else f"{_PREFIX}/{relative}"
27
+
28
+
29
+ # ── 구독·쿼리어블 핸들러 저장소 ────────────────────────────────────────────────
30
+ _sub_handlers: dict[str, list] = {}
31
+ _queryable_handlers: dict[str, object] = {}
32
+ _serve_pending: dict[str, asyncio.Future] = {}
33
+
34
+
35
+ # ── Socket.IO 이벤트 ───────────────────────────────────────────────────────────
36
+
37
+ @sio.event
38
+ async def sub(sid, data):
39
+ """브라우저가 토픽을 구독."""
40
+ await sio.enter_room(sid, f"t:{data['topic']}")
41
+
42
+
43
+ @sio.event
44
+ async def unsub(sid, data):
45
+ await sio.leave_room(sid, f"t:{data['topic']}")
46
+
47
+
48
+ @sio.event
49
+ async def pub(sid, data):
50
+ """브라우저가 발행 → Python 구독 핸들러로 전달."""
51
+ topic = data.get("topic", "")
52
+ payload = data.get("payload")
53
+ for handler in _sub_handlers.get(topic, []):
54
+ asyncio.create_task(handler(payload))
55
+
56
+
57
+ @sio.event
58
+ async def action(sid, data):
59
+ """브라우저 rb.action() 호출 → Python queryable 핸들러 → ack 반환."""
60
+ topic = data.get("topic", "")
61
+ handler = _queryable_handlers.get(topic)
62
+ if handler:
63
+ return await handler(data.get("payload"))
64
+
65
+
66
+ @sio.event
67
+ async def serve_reply(sid, data):
68
+ """브라우저가 serve_call에 응답."""
69
+ fut = _serve_pending.pop(data.get("id", ""), None)
70
+ if fut and not fut.done():
71
+ fut.set_result(data.get("payload"))
72
+
73
+
74
+ # ── 헬퍼 데코레이터 ────────────────────────────────────────────────────────────
75
+
76
+ def subscribe(relative_topic: str):
77
+ """브라우저 → Python 메시지 구독 핸들러 등록."""
78
+ def decorator(fn):
79
+ _sub_handlers.setdefault(_topic(relative_topic), []).append(fn)
80
+ return fn
81
+ return decorator
82
+
83
+
84
+ def queryable(relative_topic: str):
85
+ """브라우저 rb.action() 호출을 처리할 핸들러 등록."""
86
+ def decorator(fn):
87
+ _queryable_handlers[_topic(relative_topic)] = fn
88
+ return fn
89
+ return decorator
90
+
91
+
92
+ async def publish(relative_topic: str, payload) -> None:
93
+ """Python → 브라우저 구독자에게 메시지 발행."""
94
+ full = _topic(relative_topic)
95
+ await sio.emit("msg", {"topic": full, "payload": payload}, room=f"t:{full}")
96
+
97
+
98
+ # ── 플러그인 로직 ──────────────────────────────────────────────────────────────
99
+
100
+ @subscribe("sensor/data")
101
+ async def on_sensor_data(payload):
102
+ print(f"[__PLUGIN_NAME__] sensor/data: {payload}")
103
+
104
+
105
+ @queryable("get-status")
106
+ async def get_status(payload=None):
107
+ return {"ok": True, "plugin": "__PLUGIN_NAME__"}
108
+
109
+
110
+ # ── 진입점 ─────────────────────────────────────────────────────────────────────
111
+
112
+ if __name__ == "__main__":
113
+ print("[__PLUGIN_NAME__] Socket.IO server starting on :10000")
114
+ web.run_app(app, host="0.0.0.0", port=10000)
@@ -3,19 +3,12 @@
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__",
19
12
  "typescript": "^5.8.0",
20
13
  "vite": "^6.4.1"
21
14
  }
@@ -0,0 +1,3 @@
1
+ rainbow-rb-sdk
2
+ rainbow-rb-flat-buffers
3
+ python-socketio[aiohttp]
@@ -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
  </>
@@ -0,0 +1,25 @@
1
+ import rclpy
2
+ from rclpy.node import Node
3
+ from rb_sdk import RBManipulateSDK # PyPI: rainbow-rb-sdk (installed from requirements.txt)
4
+
5
+
6
+ sdk = RBManipulateSDK()
7
+
8
+
9
+ class __PLUGIN_CLASS__Node(Node):
10
+ def __init__(self):
11
+ super().__init__("__PLUGIN_SLUG___node")
12
+ self.get_logger().info("__PLUGIN_NAME__ node started")
13
+ # sdk is available here — use it alongside ROS2 topics as needed
14
+
15
+
16
+ def main(args=None):
17
+ rclpy.init(args=args)
18
+ node = __PLUGIN_CLASS__Node()
19
+ rclpy.spin(node)
20
+ node.destroy_node()
21
+ rclpy.shutdown()
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0"?>
2
+ <package format="3">
3
+ <name>__PLUGIN_SLUG__</name>
4
+ <version>0.0.1</version>
5
+ <description>__PLUGIN_NAME__ ROS2 node</description>
6
+ <maintainer email="todo@example.com">maintainer</maintainer>
7
+ <license>Proprietary</license>
8
+ <depend>rclpy</depend>
9
+ <export><build_type>ament_python</build_type></export>
10
+ </package>
@@ -0,0 +1,18 @@
1
+ from setuptools import setup
2
+
3
+ package_name = "__PLUGIN_SLUG__"
4
+
5
+ setup(
6
+ name=package_name,
7
+ version="0.0.1",
8
+ packages=[package_name],
9
+ data_files=[
10
+ ("share/ament_index/resource_index/packages", ["resource/" + package_name]),
11
+ ("share/" + package_name, ["package.xml"]),
12
+ ],
13
+ install_requires=["setuptools"],
14
+ zip_safe=True,
15
+ entry_points={
16
+ "console_scripts": ["__PLUGIN_SLUG___node = __PLUGIN_SLUG__.node:main"],
17
+ },
18
+ )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-dev.11",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -31,8 +31,10 @@
31
31
  "@types/react-dom": "^19.0.4"
32
32
  },
33
33
  "dependencies": {
34
- "@rainbow-robotics/preview-host": "*",
35
- "esbuild": "^0.25.0",
34
+ "@rainbow-robotics/preview-host": "0.1.2-dev.11",
35
+ "esbuild": "^0.25.0"
36
+ },
37
+ "optionalDependencies": {
36
38
  "esbuild-wasm": "^0.25.0"
37
39
  }
38
40
  }
@@ -1,21 +0,0 @@
1
- import asyncio
2
-
3
- from rb_sdk import RBManipulateSDK # PyPI: rainbow-rb-sdk
4
-
5
- sdk = RBManipulateSDK()
6
-
7
-
8
- async def on_action(payload: dict):
9
- # Matches manifest.actions[].topic — handle the "grip" action here.
10
- width = payload.get("width")
11
- print(f"[__PLUGIN_NAME__] grip width={width}")
12
- # TODO: drive the robot via the SDK.
13
-
14
-
15
- async def main():
16
- await sdk.subscribe("rby2/plugin/__PLUGIN_ID__/grip", on_action)
17
- await sdk.spin()
18
-
19
-
20
- if __name__ == "__main__":
21
- asyncio.run(main())
@@ -1 +0,0 @@
1
- rainbow-rb-sdk==__SDK_VERSION__