@rainbow-robotics/muscat-plugin-kit 1.3.2 → 1.3.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/cli/docker/Dockerfile.cpp +22 -5
- package/cli/docker/Dockerfile.dev +25 -5
- package/cli/docker/Dockerfile.python +19 -0
- package/cli/src/bridge.mjs +19 -2
- package/cli/src/build.mjs +31 -79
- package/cli/src/cpp.mjs +10 -6
- package/cli/src/dev.mjs +7 -2
- package/cli/src/devBack.mjs +44 -34
- package/cli/src/host_agent.py +143 -83
- package/cli/src/mcp.mjs +75 -16
- package/cli/src/new.mjs +5 -2
- package/cli/src/ports.mjs +21 -7
- package/cli/src/python.mjs +16 -20
- package/cli/src/ros2.mjs +13 -8
- package/cli/src/sdk.mjs +23 -21
- package/cli/src/templates-embedded.mjs +1 -1
- package/cli/src/type.mjs +42 -8
- package/cli/src/util.mjs +53 -5
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-CRNvRzJD.js → Color-YHDXOIA2-DPI4Is-F.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-C4S_Kfa9.js → Docs-CKTHz6Db.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DurNQvJo.js → Docs-CQnf2HJu.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BV0FgAF_.js → Docs-CbCty_Qg.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-axk94PIl.js → Docs-DDYksosp.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-80l_9VTs.js → Docs-Dow4_Rnl.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BgDx9SSn.js → Docs-TMe-1-I2.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-CEdKXElc.js → DocsRenderer-CFRXHY34-DSTzrCAK.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-CyZ4AE2b.js → NxAvatar.stories-B3OxXBgc.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-Bx8UtGNd.js → NxButton.stories-UTqX8ZkA.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-skizAdd5.js → NxNumberInput.stories-GoLEe-uJ.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-gtIxt2u0.js → NxPagination.stories-Jj6dt6eG.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-DSHMk3N5.js → NxPointerHoldButton.stories-tLN9u4nC.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-C1YtrFZ1.js → NxRangeInput.stories-C-MK73xb.js} +1 -1
- package/cli/storybook-dist/assets/{NxSelectBox.stories-DCySAtBp.js → NxSelectBox.stories-CBcFcOIX.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-gSaMOWo7.js → NxSlideRange.stories-oNQkoW66.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-DF0DBiyG.js → NxSwitch.stories-CqBkO5GC.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-Dill0M6z.js → NxTextArea.stories-DtysSyl4.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-Bax14r48.js → NxTextInput.stories-BOLDbSgF.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-CCwz_pIV.js → RBAccordion.stories-D9-wu9LY.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-BjDxxdPV.js → RBAccordionGroup.stories-BPfnIKm6.js} +1 -1
- package/cli/storybook-dist/assets/{RBAlert.stories-5z6GasgP.js → RBAlert.stories-CGn-c2ac.js} +1 -1
- package/cli/storybook-dist/assets/{RBBadge.stories-BwcM7i7w.js → RBBadge.stories-CBLmeoq1.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-DOeQH3rK.js → RBBattery.stories-oo47rbDh.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-DcmiOXAV.js → RBCard.stories-SiD2VfQ4.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckBox.stories-BYru87fX.js → RBCheckBox.stories-sEEVrjSg.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CXUjtR1a.js → RBCheckIconButton.stories-C5GQUjZf.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-BSOQV7Du.js → RBCheckLabelButton.stories-DTaTqBvJ.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipTab.stories-ByBmeukU.js → RBChipTab.stories-3AZr4kOz.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-GtJ01RTr.js → RBChipsBoxTab.stories-hCJnBYQl.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-C9saET1u.js → RBColorIcons.stories-BX4RJygu.js} +1 -1
- package/cli/storybook-dist/assets/{RBCommand.stories-CcvnWlWZ.js → RBCommand.stories-BidZ4H8f.js} +1 -1
- package/cli/storybook-dist/assets/{RBConfirm.stories-hdBVSdBS.js → RBConfirm.stories-D3Txw1kT.js} +1 -1
- package/cli/storybook-dist/assets/{RBDateInput.stories-BE9JQl-E.js → RBDateInput.stories-CLxRaN6T.js} +1 -1
- package/cli/storybook-dist/assets/{RBDialog.stories-BaWuDgl_.js → RBDialog.stories-CEhmUGp-.js} +1 -1
- package/cli/storybook-dist/assets/{RBDivider.stories-D3INKm_Z.js → RBDivider.stories-peXP2KG9.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-BZ8V0J6-.js → RBDrawer.stories-38iDf6uC.js} +1 -1
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-DxLi8HRw.js → RBDropdownMenu.stories-Dsi8tqSy.js} +1 -1
- package/cli/storybook-dist/assets/{RBEmptyView.stories-DQcvDt2S.js → RBEmptyView.stories-VMxWhzLP.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-DJfuTpQ0.js → RBIconButton.stories-BPLhceGO.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-BSsyb0BK.js → RBIcons.stories-DuYdO951.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-Di3th--x.js → RBInlineLabelButton.stories-CGNYhQyl.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-DfGazm4n.js → RBJoyStick.stories-CCmn-LMy.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-CLbxzW56.js → RBLabelButton.stories-IkKjthGk.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-DeiuDm_P.js → RBLight.stories-eJFQOjPH.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-BZQTV9e5.js → RBMenubar.stories-kg-xXGrW.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DDNmdi_n.js → RBMultiRangeInput.stories-Czr1r3gd.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-CLBCEyCU.js → RBPagination.stories-Dyu6Khnl.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-DRI5Kfa-.js → RBPopover.stories-CAM6hpf2.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-DIAmypoJ.js → RBPrompt.stories-B4M-veaf.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-BS_N4aLf.js → RBRadioBox.stories-DeAOKvil.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-BTm8cYvm.js → RBRadioButton.stories-DjXwYXFy.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-D5hBsSyB.js → RBRangeDateInput.stories-9pzKhXGf.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-CxPEkbXQ.js → RBResizable.stories-B4Hi4DUD.js} +1 -1
- package/cli/storybook-dist/assets/{RBSelectBox.stories-FUQ7AcUi.js → RBSelectBox.stories-tVVv4c6z.js} +1 -1
- package/cli/storybook-dist/assets/{RBSheet.stories-mhVHH_71.js → RBSheet.stories-BwOCmaNN.js} +1 -1
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-5FMkLUim.js → RBSideNumberInput.stories-BbGOIitU.js} +1 -1
- package/cli/storybook-dist/assets/{RBSlider.stories-Bnhcp9pz.js → RBSlider.stories-yIDdawYQ.js} +1 -1
- package/cli/storybook-dist/assets/{RBSpinner.stories-Bf4416mP.js → RBSpinner.stories-CIJvpbgK.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-BNhFjRaV.js → RBSwitch.stories-Bo8hrSiq.js} +1 -1
- package/cli/storybook-dist/assets/{RBTab.stories-Bsr7jlfb.js → RBTab.stories-Cq3N4Dgb.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-BkA6pKzs.js → RBTable.stories-DDeZf-Wb.js} +1 -1
- package/cli/storybook-dist/assets/{RBTableRow.stories-D07Z7D5-.js → RBTableRow.stories-BeKVMSo3.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-DHnZiV21.js → RBTabs.stories-DovB7PPB.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextArea.stories-CtLMLuV_.js → RBTextArea.stories-D7H4RvP8.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextField.stories-Chz0i1-u.js → RBTextField.stories-CmkNXeEk.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextInput.stories-Dk5cZfUF.js → RBTextInput.stories-hnxbQJsb.js} +1 -1
- package/cli/storybook-dist/assets/{RBToast.stories-tZxbjmr8.js → RBToast.stories-BklGGkNh.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-VTe7a4xZ.js → RBToggle.stories-C56AXyxq.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-WiFnGGYx.js → RBToggleGroup.stories-CpLPdAF0.js} +1 -1
- package/cli/storybook-dist/assets/{RBTooltip.stories-B_L4W7Mn.js → RBTooltip.stories-BdOnCwGk.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-os5wKKf7.js → SideNumberInput-DLmM2_Dy.js} +80 -80
- package/cli/storybook-dist/assets/{entry-preview-COJgWzHb.js → entry-preview-tBizwvut.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-DQEjJuXA.js → iframe-BVXraKDc.js} +2 -2
- package/cli/storybook-dist/assets/{index-DEgQqQ_-.js → index-CI3FxSvF.js} +3 -3
- package/cli/storybook-dist/assets/{preview-BZ5PpEp_.js → preview-BUOha-H6.js} +2 -2
- package/cli/storybook-dist/assets/preview-DGIRF99V.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 +65 -4
- package/cli/templates/common/py/main.py +5 -1
- package/cli/templates/common/requirements.txt +3 -3
- package/cli/templates/cpp/cpp/CMakeLists.txt +1 -2
- package/cli/templates/cpp/cpp/main.cpp +48 -8
- package/cli/templates/readme-cpp/README.md +969 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +1 -1
- package/package.json +3 -3
- package/cli/storybook-dist/assets/preview-CmCb8Xea.js +0 -3
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
# __PLUGIN_NAME__
|
|
2
|
+
|
|
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 (C++ rebuild + restart in 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
|
+
│ └─ style.css # plugin styles auto-loaded by muscat()
|
|
23
|
+
├─ cpp/
|
|
24
|
+
│ ├─ CMakeLists.txt # links rb_sdk::rb_sdk (C++ SDK)
|
|
25
|
+
│ └─ main.cpp # C++ backend entry point
|
|
26
|
+
├─ assets/
|
|
27
|
+
│ └─ icon.svg # icon referenced from manifest
|
|
28
|
+
├─ manifest.json # metadata, contribution points, runtime config
|
|
29
|
+
└─ README.md # this file (copied to dist/ on build)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> ROS2 (rclcpp) plugins have `ros2_ws/` instead of `cpp/`.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Frontend (`ui/content.tsx`)
|
|
37
|
+
|
|
38
|
+
### JavaScript dependencies
|
|
39
|
+
|
|
40
|
+
`muscat-pk build` runs this plugin project's own Vite build. Vite resolves
|
|
41
|
+
imports from installed `node_modules`; it does not care whether a package is
|
|
42
|
+
listed under `dependencies` or `devDependencies`, but the package must be
|
|
43
|
+
installed.
|
|
44
|
+
|
|
45
|
+
Do not run a production-only install such as `npm install --omit=dev` before
|
|
46
|
+
building the plugin UI, because build tools like `vite`, `typescript`,
|
|
47
|
+
`tailwindcss`, `@tailwindcss/vite`, `sass`, or `less` are usually
|
|
48
|
+
`devDependencies`.
|
|
49
|
+
|
|
50
|
+
The scaffold keeps this split:
|
|
51
|
+
|
|
52
|
+
| package kind | examples | why |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `dependencies` | `@rainbow-robotics/plugin-ui`, UI libraries imported by plugin source | Author-facing APIs used by `ui/*.tsx` |
|
|
55
|
+
| `devDependencies` | `vite`, `typescript`, `tailwindcss`, `@tailwindcss/vite`, `sass`, `less` | Build/dev tools needed before `dist/*.js` is produced |
|
|
56
|
+
|
|
57
|
+
The built plugin ZIP does not include `package.json` or `node_modules`. For
|
|
58
|
+
production, Muscat provides `@rainbow-robotics/plugin-ui`, React, and ReactDOM
|
|
59
|
+
through the host import map; plugin UI build output imports those as host
|
|
60
|
+
externals.
|
|
61
|
+
|
|
62
|
+
### Styles
|
|
63
|
+
|
|
64
|
+
`muscat()` automatically loads `ui/style.{css,scss,sass,less}` (or
|
|
65
|
+
`ui/styles.{css,scss,sass,less}`) for every UI entry, plus
|
|
66
|
+
`ui/{entry}.{css,scss,sass,less}` for a matching entry-specific stylesheet. You
|
|
67
|
+
do not need to import CSS from every `.tsx` file.
|
|
68
|
+
|
|
69
|
+
The scaffold uses Tailwind's theme/utilities layers only, so plugin utilities
|
|
70
|
+
are generated from files under `ui/` without applying Tailwind preflight resets
|
|
71
|
+
to the host page.
|
|
72
|
+
|
|
73
|
+
Built CSS selectors are scoped under the current plugin id
|
|
74
|
+
(`[data-muscat-plugin-id="..."]`). CSS-in-JS libraries such as Emotion work
|
|
75
|
+
normally, but global styles should be scoped by the author.
|
|
76
|
+
|
|
77
|
+
#### Tailwind
|
|
78
|
+
|
|
79
|
+
This template enables Tailwind in `vite.config.ts`:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { defineConfig } from "vite";
|
|
83
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
84
|
+
import { muscat } from "@rainbow-robotics/muscat-plugin-kit/vite";
|
|
85
|
+
|
|
86
|
+
export default defineConfig({
|
|
87
|
+
plugins: [tailwindcss(), muscat()],
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The default `ui/style.css` uses Tailwind theme/utilities only, not preflight:
|
|
92
|
+
|
|
93
|
+
```css
|
|
94
|
+
@layer theme, utilities;
|
|
95
|
+
|
|
96
|
+
@import "tailwindcss/theme.css" layer(theme);
|
|
97
|
+
@import "tailwindcss/utilities.css" layer(utilities);
|
|
98
|
+
|
|
99
|
+
@source "./**/*.{ts,tsx}";
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### SCSS, Sass, and Less
|
|
103
|
+
|
|
104
|
+
Install the preprocessor package, then create one of the auto-loaded style files.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm i -D sass
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```scss
|
|
111
|
+
/* ui/style.scss */
|
|
112
|
+
$accent: #3b82f6;
|
|
113
|
+
|
|
114
|
+
.panel {
|
|
115
|
+
border: 1px solid color-mix(in srgb, $accent 40%, transparent);
|
|
116
|
+
|
|
117
|
+
&__title {
|
|
118
|
+
color: $accent;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For Less, install `less` and use `ui/style.less`.
|
|
124
|
+
|
|
125
|
+
#### Emotion
|
|
126
|
+
|
|
127
|
+
Emotion dependencies may be in `dependencies` or `devDependencies`; Vite can
|
|
128
|
+
bundle them when they are installed and imported by the plugin.
|
|
129
|
+
|
|
130
|
+
For `styled`, no Vite config change is needed:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm i @emotion/react @emotion/styled
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import styled from "@emotion/styled";
|
|
138
|
+
|
|
139
|
+
const Panel = styled.div({
|
|
140
|
+
display: "grid",
|
|
141
|
+
gap: 12,
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For Emotion's `css` prop, pass React plugin options through `muscat()` and add
|
|
146
|
+
`jsxImportSource` to `tsconfig.json`:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm i @emotion/react @emotion/styled
|
|
150
|
+
npm i -D @emotion/babel-plugin
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// vite.config.ts
|
|
155
|
+
import { defineConfig } from "vite";
|
|
156
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
157
|
+
import { muscat } from "@rainbow-robotics/muscat-plugin-kit/vite";
|
|
158
|
+
|
|
159
|
+
export default defineConfig({
|
|
160
|
+
plugins: [
|
|
161
|
+
tailwindcss(),
|
|
162
|
+
muscat({
|
|
163
|
+
react: {
|
|
164
|
+
jsxImportSource: "@emotion/react",
|
|
165
|
+
babel: {
|
|
166
|
+
plugins: ["@emotion/babel-plugin"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"compilerOptions": {
|
|
177
|
+
"jsxImportSource": "@emotion/react"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Emotion `Global` writes global CSS at runtime, so scope it manually or avoid it
|
|
183
|
+
inside plugins.
|
|
184
|
+
|
|
185
|
+
### useRb() hook
|
|
186
|
+
|
|
187
|
+
Access the communication API between the plugin module and UI from any plugin component.
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
|
|
191
|
+
|
|
192
|
+
function App() {
|
|
193
|
+
const rb = useRb();
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### rb API
|
|
198
|
+
|
|
199
|
+
| Method | Description |
|
|
200
|
+
| ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
201
|
+
| `rb.pub(topic, payload)` | Publish a message to a topic |
|
|
202
|
+
| `rb.sub(topic, handler)` | Subscribe to a topic — returns `{ undeclare() }` |
|
|
203
|
+
| `rb.action(topic, payload?)` | Send a request and await a reply |
|
|
204
|
+
| `rb.serve(topic, handler)` | Handle incoming requests from the backend |
|
|
205
|
+
| `rb.toast({ title?, message, type?, actionLabel?, handleActionClick?, closeOnActionClick?, duration?, onClose? })` | Show a host toast notification. `message` accepts a string or `ReactElement`. `type` can be `"info"`, `"danger"`, `"warning"`, `"caution"`, or `"success"` |
|
|
206
|
+
| `rb.alert({ title?, message, buttonText? })` | Show a host alert dialog. Resolves when the dialog is closed |
|
|
207
|
+
| `rb.confirm({ title?, message, buttonText? })` | Show a host confirmation dialog. `buttonText` accepts `{ yes, no }`. Resolves to `true` or `false` |
|
|
208
|
+
| `rb.prompt({ title?, message, value?, placeholder?, buttonText? })` | Show a host prompt dialog. `buttonText` accepts `{ yes, no }`. Resolves to the entered string or `undefined` |
|
|
209
|
+
|
|
210
|
+
Topic names passed to `pub`, `sub`, `action`, and `serve` are isolated per plugin.
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
All methods support Generic Types:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
const status = await rb.action<{ ok: boolean }, { id: number }>(
|
|
217
|
+
"get-status",
|
|
218
|
+
{ id: 1 },
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const sub = await rb.sub<{ value: number }>("sensor/data", (data) => {
|
|
222
|
+
console.log(data.value);
|
|
223
|
+
});
|
|
224
|
+
sub.undeclare(); // cleanup
|
|
225
|
+
|
|
226
|
+
const ok = await rb.confirm({
|
|
227
|
+
title: "Run",
|
|
228
|
+
message: "Start the backend action?",
|
|
229
|
+
});
|
|
230
|
+
if (ok) {
|
|
231
|
+
await rb.action("run");
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Saving and restoring form data (`useRbFormData` / `useRbSave`)
|
|
236
|
+
|
|
237
|
+
If the host embeds your plugin in a form it owns (e.g. a program step editor
|
|
238
|
+
with its own Save button), it can hand your plugin previously-saved values and
|
|
239
|
+
pull the current values back out when the user saves. The host — not the
|
|
240
|
+
plugin — persists the data (IndexedDB, backend, etc.).
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
import { useState } from "react";
|
|
244
|
+
import { useRbFormData, useRbSave } from "@rainbow-robotics/plugin-ui/react";
|
|
245
|
+
|
|
246
|
+
type GripArgs = { targetName: string; width: number };
|
|
247
|
+
const DEFAULTS: GripArgs = { targetName: "", width: 20 };
|
|
248
|
+
|
|
249
|
+
export default function App() {
|
|
250
|
+
// Previously-saved values, or undefined for a brand-new instance.
|
|
251
|
+
const saved = useRbFormData<GripArgs>();
|
|
252
|
+
const [args, setArgs] = useState<GripArgs>({ ...DEFAULTS, ...saved });
|
|
253
|
+
|
|
254
|
+
// Registers a getter the host calls when its Save button is clicked.
|
|
255
|
+
// Always returns the CURRENT values — re-registering on every render keeps
|
|
256
|
+
// the closure fresh, which useRbSave already handles for you.
|
|
257
|
+
useRbSave(() => ({
|
|
258
|
+
args,
|
|
259
|
+
summary: `${args.targetName || "target"} · ${args.width}mm`,
|
|
260
|
+
}));
|
|
261
|
+
|
|
262
|
+
return (/* ...form UI reading/writing `args`... */);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The value returned from `useRbSave` must be JSON-serialisable — the host
|
|
267
|
+
persists it as-is. Not every host embeds plugins in a save/restore form; if
|
|
268
|
+
yours doesn't, you can skip these hooks entirely.
|
|
269
|
+
|
|
270
|
+
### UI components
|
|
271
|
+
|
|
272
|
+
Import Muscat components from `@rainbow-robotics/plugin-ui/react` and export a
|
|
273
|
+
React component as the default export. The host mounts that component directly
|
|
274
|
+
and provides `useRb()` through context.
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { useState } from "react";
|
|
278
|
+
import { RBLabelButton, RBSlider, RBSwitch } from "@rainbow-robotics/plugin-ui/react";
|
|
279
|
+
import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
|
|
280
|
+
|
|
281
|
+
export default function App() {
|
|
282
|
+
const rb = useRb();
|
|
283
|
+
const [width, setWidth] = useState(20);
|
|
284
|
+
const [enabled, setEnabled] = useState(true);
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div className="flex flex-col gap-4">
|
|
288
|
+
<RBSlider
|
|
289
|
+
value={[width]}
|
|
290
|
+
min={0}
|
|
291
|
+
max={100}
|
|
292
|
+
onValueChange={(values: number[]) => setWidth(values[0] ?? 0)}
|
|
293
|
+
/>
|
|
294
|
+
<RBSwitch checked={enabled} onCheckedChange={setEnabled} />
|
|
295
|
+
<RBLabelButton
|
|
296
|
+
disabled={!enabled}
|
|
297
|
+
onClick={() => rb.action("grip", { width })}
|
|
298
|
+
>
|
|
299
|
+
Grip
|
|
300
|
+
</RBLabelButton>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Developing directly on the robot PC
|
|
309
|
+
|
|
310
|
+
If you're running `muscat-pk dev` **on the robot PC itself**, Muscat is likely already
|
|
311
|
+
running its own Socket.IO bridge on port 10000. In that case the CLI detects the port
|
|
312
|
+
conflict and asks:
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
⚠ port 10000 is already in use — can't start Socket.IO bridge.
|
|
316
|
+
Proceed without Socket.IO bridge? [y/N]:
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Answer **y**. Your plugin backend will connect to the Muscat bridge that's already
|
|
320
|
+
running instead of starting a new one — exactly what you want when developing on the
|
|
321
|
+
robot.
|
|
322
|
+
|
|
323
|
+
> If you answer **n** (or just press Enter), the CLI exits so you can free the port
|
|
324
|
+
> manually and retry.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Backend runtimes
|
|
329
|
+
|
|
330
|
+
`manifest.json` chooses one backend runtime. `muscat-pk dev` runs backend code in
|
|
331
|
+
a Docker container matching the robot environment. Backend code should contain
|
|
332
|
+
plugin logic only; it should not start the Muscat bridge or its own copy of the
|
|
333
|
+
plugin gateway.
|
|
334
|
+
|
|
335
|
+
### Which backend pattern? (depends on the contribution point)
|
|
336
|
+
|
|
337
|
+
**How you control the robot depends on what your plugin contributes** (see
|
|
338
|
+
`manifest.json` → `contributes[].point`). Don't mix these up:
|
|
339
|
+
|
|
340
|
+
| Contribution point | What it is | Backend pattern |
|
|
341
|
+
| --- | --- | --- |
|
|
342
|
+
| `program.panel` (with `commandInfo`) | Defines a **custom command** the plugin adds to the Program page's rule-based sequence builder. The operator places it into the program tree like any built-in command (Move J, Point, Wait, …); it runs when the program reaches that step. | **`PluginProgramContext`** — integrated with the program's stop/pause and variables. See _Program command backend_ below. |
|
|
343
|
+
| `toolbar` / `lnb` / `move.panel` | A live UI panel/popover the operator interacts with directly | Call the robot **SDK directly** (`RBManipulateSDK`) from `cpp/main.cpp`, or `rb.action(...)` → your backend → SDK. **Not** a program step — there is no stop/pause/variable context. |
|
|
344
|
+
|
|
345
|
+
Rule of thumb: use `PluginProgramContext` **only** for a `program.panel` command
|
|
346
|
+
that runs as part of a program. To move the robot live from a toolbar/move panel,
|
|
347
|
+
use the SDK — a program-step context does not apply.
|
|
348
|
+
|
|
349
|
+
### Frontend-only (`none`)
|
|
350
|
+
|
|
351
|
+
Use this when the plugin only has UI and calls host services:
|
|
352
|
+
|
|
353
|
+
```json
|
|
354
|
+
{ "runtime": { "type": "none" } }
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### C++ (`cpp`)
|
|
358
|
+
|
|
359
|
+
C++ plugins build `cpp/` with CMake and run the executable named by
|
|
360
|
+
`manifest.json` → `runtime.entry`. The Rainbow Robotics C++ SDK
|
|
361
|
+
(`rainbow-rb-sdk-dev`) is pre-installed in the `muscat-pk` dev container — you
|
|
362
|
+
don't install anything locally.
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{ "runtime": { "type": "cpp", "entry": "__PLUGIN_SLUG__" } }
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
```cmake
|
|
369
|
+
# cpp/CMakeLists.txt
|
|
370
|
+
find_package(rb_sdk REQUIRED)
|
|
371
|
+
add_executable(__PLUGIN_SLUG__ main.cpp) # name must equal runtime.entry
|
|
372
|
+
target_link_libraries(__PLUGIN_SLUG__ PRIVATE rb_sdk::rb_sdk)
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Headers live under `<rainbow/rb_sdk/...>`:
|
|
376
|
+
|
|
377
|
+
| Header | Class | Use |
|
|
378
|
+
| --- | --- | --- |
|
|
379
|
+
| `plugin/rb_plugin.hpp` | `rb_plugin::RBPluginCall` | plugin UI ↔ backend, external endpoints (`router()`) |
|
|
380
|
+
| `manipulate/rb_manipulate_sdk.hpp` | `RBManipulateSDK` | control a manipulator |
|
|
381
|
+
| `program/rb_program_sdk.hpp` | `RBProgramSDK` | program-side helpers |
|
|
382
|
+
| `program/plugin_program_context.hpp` | `rb_plugin::PluginProgramContext` | `program.panel` commands |
|
|
383
|
+
| `zenoh_client.hpp` | `ZenohClient` | low-level pub/sub/query |
|
|
384
|
+
|
|
385
|
+
Payloads are `nlohmann::json` (`#include <nlohmann/json.hpp>`, installed with the SDK).
|
|
386
|
+
|
|
387
|
+
#### Talking to the plugin UI (`RBPluginCall`)
|
|
388
|
+
|
|
389
|
+
```cpp
|
|
390
|
+
#include <rainbow/rb_sdk/plugin/rb_plugin.hpp>
|
|
391
|
+
#include <nlohmann/json.hpp>
|
|
392
|
+
|
|
393
|
+
using nlohmann::json;
|
|
394
|
+
using rb_plugin::RBPluginCall;
|
|
395
|
+
|
|
396
|
+
int main() {
|
|
397
|
+
// plugin id comes from env RB_PLUGIN_ID (set by muscat-pk dev and by the robot)
|
|
398
|
+
auto plugin = RBPluginCall::get_instance(RBPluginCall::resolve_plugin_id(std::nullopt));
|
|
399
|
+
|
|
400
|
+
plugin->publish("state", {{"ready", true}}); // UI: rb.sub("state", ...)
|
|
401
|
+
|
|
402
|
+
auto sub = plugin->subscribe("event", // UI: rb.pub("event", ...)
|
|
403
|
+
[](const std::string& topic, const json& payload) { /* ... */ });
|
|
404
|
+
|
|
405
|
+
plugin->serve("tool/echo", [](const json& payload) { // UI: await rb.action("tool/echo", ...)
|
|
406
|
+
return json{{"echo", payload}};
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
json res = plugin->call("tool/ping", {{"hello", "plugin"}}, 5.0); // UI answers with rb.serve("tool/ping", ...)
|
|
410
|
+
|
|
411
|
+
for (;;) std::this_thread::sleep_for(std::chrono::seconds(1)); // stay resident
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
| C++ | UI (`useRb()`) |
|
|
416
|
+
| --- | --- |
|
|
417
|
+
| `plugin->serve(topic, fn)` | `await rb.action(topic, payload)` |
|
|
418
|
+
| `plugin->subscribe(topic, fn)` | `rb.pub(topic, payload)` |
|
|
419
|
+
| `plugin->publish(topic, payload)` | `rb.sub(topic, handler)` |
|
|
420
|
+
| `plugin->call(topic, payload, timeout_sec)` | `rb.serve(topic, handler)` |
|
|
421
|
+
|
|
422
|
+
- Keep the `Subscription` returned by `subscribe`; pass it to `plugin->unsubscribe(std::nullopt, sub)` to remove just that one.
|
|
423
|
+
- `call` throws `rb_plugin::RBPluginGatewayError` when the gateway is down or the UI doesn't answer in time.
|
|
424
|
+
|
|
425
|
+
#### External endpoints (`router()`)
|
|
426
|
+
|
|
427
|
+
`serve` / `subscribe` / `publish` are **internal only** (plugin UI ↔ backend).
|
|
428
|
+
To expose an endpoint to the outside world — REST, Socket.IO, TCP, MQTT,
|
|
429
|
+
WebRTC — use `plugin->router()`. The running Muscat (`common`) picks it up
|
|
430
|
+
without a restart, and it disappears when the plugin stops.
|
|
431
|
+
|
|
432
|
+
```cpp
|
|
433
|
+
auto& router = plugin->router();
|
|
434
|
+
|
|
435
|
+
router.on("/hello", [](const json& payload, const json& meta) { // all 5 transports
|
|
436
|
+
return json{{"msg", "hi " + payload.value("name", std::string("there"))}};
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
router.on("/admin", [](const json& payload, const json& meta) {
|
|
440
|
+
if (!meta.value("principal", json::object()).value("is_admin", false))
|
|
441
|
+
throw rb_plugin::PluginRouteError(403, "admin only"); // becomes the external status
|
|
442
|
+
return json::object();
|
|
443
|
+
}, std::vector<std::string>{"api"}); // REST only
|
|
444
|
+
|
|
445
|
+
router.subscribe("/cmd", [](const json& payload) { /* ... */ },
|
|
446
|
+
std::vector<std::string>{"tcp"}); // receive external TCP publish
|
|
447
|
+
|
|
448
|
+
router.emit("/state", {{"busy", true}}); // push to external subscribers
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
curl -s -X POST http://<robot>/common/plugin/<plugin_id>/hello \
|
|
453
|
+
-H 'Content-Type: application/json' -d '{"name":"kim"}'
|
|
454
|
+
# → {"msg":"hi kim"}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
| API | Meaning | External address |
|
|
458
|
+
| --- | --- | --- |
|
|
459
|
+
| `router.on(path, handler, explicit_routes)` | request/response handler | `common/plugin/{plugin_id}/{path}` (REST is always `POST`) |
|
|
460
|
+
| `router.subscribe(topic, handler, explicit_events)` | receive external publishes | external side publishes to `common/plugin/{plugin_id}/{topic}` |
|
|
461
|
+
| `router.emit(topic, payload)` | plugin → external push | external side subscribes to `common/plugin/{plugin_id}/{topic}` |
|
|
462
|
+
|
|
463
|
+
- `meta` = `{"transport": "api"|"socket"|"tcp"|"mqtt"|"webrtc", "principal": object | null}`. **Role checks are the plugin's job.**
|
|
464
|
+
- Transport names: `api | socket | tcp | mqtt | webrtc`; `subscribe` accepts only `tcp | mqtt | webrtc`. Unknown names or duplicate paths throw `std::invalid_argument`.
|
|
465
|
+
- Throw `rb_plugin::PluginRouteError(code, message)` to set the status; any other exception → 500.
|
|
466
|
+
|
|
467
|
+
| Situation | Status |
|
|
468
|
+
| --- | --- |
|
|
469
|
+
| No plugin declares that path / plugin is down / transport not allowed | 404 |
|
|
470
|
+
| Plugin did not answer within 10 s | 504 |
|
|
471
|
+
| `common` ↔ gateway not connected | 503 |
|
|
472
|
+
|
|
473
|
+
During `muscat-pk dev` the local bridge accepts `router()` registrations, but
|
|
474
|
+
**external calls need a running `common`** (a robot or a local `common`). After
|
|
475
|
+
restarting the plugin, external calls may return 504 for about a minute, then 404.
|
|
476
|
+
|
|
477
|
+
#### Program command backend (`program.panel`)
|
|
478
|
+
|
|
479
|
+
A `program.panel` contribution adds a **custom command** to the Program page.
|
|
480
|
+
Each time the program reaches that step, the host calls your backend through
|
|
481
|
+
`PluginProgramContext` with the step args (what the UI saved with `useRbSave`).
|
|
482
|
+
|
|
483
|
+
```cpp
|
|
484
|
+
#include <rainbow/rb_sdk/program/plugin_program_context.hpp>
|
|
485
|
+
|
|
486
|
+
using rb_plugin::PluginProgramContext;
|
|
487
|
+
|
|
488
|
+
int main() {
|
|
489
|
+
PluginProgramContext ctx("__PLUGIN_ID__");
|
|
490
|
+
|
|
491
|
+
ctx.set_execute_handler([&](const nlohmann::json& args) {
|
|
492
|
+
auto count = ctx.get_variable("counter"); // json; null if no program runs
|
|
493
|
+
ctx.set_variable("counter", count.is_number() ? count.get<int>() + 1 : 1);
|
|
494
|
+
ctx.set_variable("RBP___PLUGIN_SLUG___result", 42); // NEW variables must start with RBP_
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
for (int i = 0; i < 100; ++i) {
|
|
498
|
+
ctx.check_stop(); // pause → blocks, stop → throws PluginStopped
|
|
499
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
500
|
+
}
|
|
501
|
+
} catch (const rb_plugin::PluginStopped&) {
|
|
502
|
+
return; // program was stopped
|
|
503
|
+
}
|
|
504
|
+
ctx.done(); // always call — the step ends here
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
ctx.start(); // does not block
|
|
508
|
+
for (;;) std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
- The step does not finish until you call `ctx.done()`.
|
|
513
|
+
- `set_execute_handler` must be called before `start()`.
|
|
514
|
+
- **New** variables need the `RBP_` prefix (`RBP_{plugin_id}_{name}` recommended); existing ones pass as-is.
|
|
515
|
+
- `get_variable(name, robot_model, plugin_id)` — optional filters: a specific robot, or the value a given plugin last wrote.
|
|
516
|
+
- Executor state: `ctx.executor_state_for("C500920")`, `ctx.executor_states()`, `ctx.is_paused()`, `ctx.is_stopped()`,
|
|
517
|
+
or `ctx.set_state_change_handler([](const std::string& model, const std::string& state) { ... })`.
|
|
518
|
+
- A long-running handler must call `check_stop()` every iteration, or it keeps running after the operator presses Stop.
|
|
519
|
+
|
|
520
|
+
Variables the Program page variable picker should offer must be declared in
|
|
521
|
+
`manifest.json` under `"variables"`.
|
|
522
|
+
|
|
523
|
+
#### Controlling the robot
|
|
524
|
+
|
|
525
|
+
```cpp
|
|
526
|
+
#include <rainbow/rb_sdk/manipulate/rb_manipulate_sdk.hpp>
|
|
527
|
+
|
|
528
|
+
RBManipulateSDK manipulate; // on the robot: connects to the local Zenoh
|
|
529
|
+
manipulate.state.call_servocontrol("C500920", 1);
|
|
530
|
+
|
|
531
|
+
IPC::MoveInput_TargetT t;
|
|
532
|
+
t.tar_values = {0, 0, 90, 0, 90, 0};
|
|
533
|
+
t.tar_frame = 0;
|
|
534
|
+
t.tar_unit = 1;
|
|
535
|
+
manipulate.move.call_move_j("C500920", t, std::nullopt, MoveSpeedArg{1, 60.f, 120.f});
|
|
536
|
+
manipulate.move.call_move_wait("C500920", -1);
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Remote dev (`muscat-pk dev --robot-ip <ip> --robot-token <token>`) sets
|
|
540
|
+
`MUSCAT_ROBOT_HOST` / `MUSCAT_ROBOT_TOKEN`; connect **before** registering
|
|
541
|
+
subscriptions, because `connect()` reopens the Zenoh session:
|
|
542
|
+
|
|
543
|
+
```cpp
|
|
544
|
+
if (const char* host = std::getenv("MUSCAT_ROBOT_HOST"))
|
|
545
|
+
if (const char* token = std::getenv("MUSCAT_ROBOT_TOKEN"))
|
|
546
|
+
manipulate.connect({.token = token, .host = host});
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
AMR and RB-Y1 SDKs are Python-only for now.
|
|
550
|
+
|
|
551
|
+
### ROS2 (rclcpp)
|
|
552
|
+
|
|
553
|
+
ROS2 C++ plugins use `ros2_ws/` with `runtime.rosClient: "rclcpp"`. The node
|
|
554
|
+
links the C++ SDK the same way (`find_package(rb_sdk)` + `rb_sdk::rb_sdk`) and is
|
|
555
|
+
launched with `--ros-args -r __ns:=/{slug}` automatically.
|
|
556
|
+
|
|
557
|
+
```cpp
|
|
558
|
+
#include "rclcpp/rclcpp.hpp"
|
|
559
|
+
#include <rainbow/rb_sdk/manipulate/rb_manipulate_sdk.hpp>
|
|
560
|
+
|
|
561
|
+
class __PLUGIN_CLASS__Node : public rclcpp::Node {
|
|
562
|
+
public:
|
|
563
|
+
__PLUGIN_CLASS__Node() : Node("__PLUGIN_SLUG___node") {
|
|
564
|
+
RCLCPP_INFO(this->get_logger(), "__PLUGIN_NAME__ node started");
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
int main(int argc, char** argv) {
|
|
569
|
+
rclcpp::init(argc, argv);
|
|
570
|
+
rclcpp::spin(std::make_shared<__PLUGIN_CLASS__Node>());
|
|
571
|
+
rclcpp::shutdown();
|
|
572
|
+
return 0;
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
`RBPluginCall` and `PluginProgramContext` work in an rclcpp node too — create
|
|
577
|
+
them in `main()` before `rclcpp::spin`.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## Video streaming (MediaMTX)
|
|
582
|
+
|
|
583
|
+
Muscat runs a MediaMTX media server. The backend publishes a stream over RTSP and
|
|
584
|
+
the UI plays it over WebRTC. `muscat-pk dev` runs the same server (or uses a local
|
|
585
|
+
Muscat's), so the addresses are identical in dev and on the robot.
|
|
586
|
+
|
|
587
|
+
| | Address |
|
|
588
|
+
| --- | --- |
|
|
589
|
+
| Publish (backend) | `rtsp://127.0.0.1:8554/<plugin_id>/<name>` |
|
|
590
|
+
| Play (UI) | `http://<host>:8889/<plugin_id>/<name>/` |
|
|
591
|
+
|
|
592
|
+
- Prefix the path with your plugin id — paths are shared with robot cameras and other plugins.
|
|
593
|
+
- The robot runtime has no ffmpeg, GStreamer or OpenCV; bring your own encoder (below).
|
|
594
|
+
|
|
595
|
+
**Backend (C++)** — publish RTSP with an encoder library you bring (e.g. FFmpeg's
|
|
596
|
+
libavformat / libx264); the robot has none installed. Ship it with the plugin — see
|
|
597
|
+
_Bundling extra shared libraries_.
|
|
598
|
+
|
|
599
|
+
**UI** — embed MediaMTX's WebRTC player. `location.hostname` is `localhost` in the
|
|
600
|
+
preview and the robot's address on the robot:
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
const STREAM = "__PLUGIN_ID__/cam"; // the path the backend publishes
|
|
604
|
+
|
|
605
|
+
export default function Camera() {
|
|
606
|
+
return (
|
|
607
|
+
<iframe
|
|
608
|
+
src={`${location.protocol}//${location.hostname}:8889/${STREAM}/`}
|
|
609
|
+
allow="autoplay"
|
|
610
|
+
className="aspect-video w-full border-0"
|
|
611
|
+
/>
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## manifest.json
|
|
619
|
+
|
|
620
|
+
```jsonc
|
|
621
|
+
{
|
|
622
|
+
// unique plugin ID — reverse-domain format recommended
|
|
623
|
+
"id": "com.example.my-plugin",
|
|
624
|
+
"name": "My Plugin",
|
|
625
|
+
"version": "1.0.0",
|
|
626
|
+
|
|
627
|
+
// minimum Muscat version required to load this plugin
|
|
628
|
+
"minMuscatVersion": "1.0.0",
|
|
629
|
+
|
|
630
|
+
// plugin author (empty string is fine)
|
|
631
|
+
"author": "Your Name",
|
|
632
|
+
|
|
633
|
+
// displayed in the Muscat plugin store / info panel
|
|
634
|
+
// string or { en (required), ko, ja, zh, vi, th, es }
|
|
635
|
+
"description": { "en": "Plugin description", "ko": "플러그인 설명" },
|
|
636
|
+
|
|
637
|
+
// SVG string (single file) or PNG/JPG size map (all three sizes required)
|
|
638
|
+
"icons": "assets/icon.svg",
|
|
639
|
+
// "icons": { "16": "assets/icon-16.png", "48": "assets/icon-48.png", "128": "assets/icon-128.png" },
|
|
640
|
+
|
|
641
|
+
// targetModels and/or targetCategorys — at least one is required
|
|
642
|
+
// "all" = every target, or list specific values
|
|
643
|
+
"targetModels": "all",
|
|
644
|
+
// "targetModels": ["C500920", "C500930"],
|
|
645
|
+
"targetCategorys": "all",
|
|
646
|
+
// "targetCategorys": ["manipulate", "amr", "rby1", "vision"],
|
|
647
|
+
|
|
648
|
+
"contributes": [
|
|
649
|
+
// program.panel — defines a custom command for the Program page sequence
|
|
650
|
+
// builder; runs via PluginProgramContext when the program reaches it
|
|
651
|
+
{
|
|
652
|
+
"point": "program.panel",
|
|
653
|
+
"icon": "assets/icon.svg",
|
|
654
|
+
"label": { "ko": "내 플러그인", "en": "My Plugin" },
|
|
655
|
+
"ui": "program", // must be unique across all contributions
|
|
656
|
+
"commandInfo": {
|
|
657
|
+
"name": "My Plugin",
|
|
658
|
+
"icon": "assets/icon.svg",
|
|
659
|
+
"funcName": "rb_program_sdk.plugin_execute",
|
|
660
|
+
"args": {}
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
// toolbar — adds a button to the top toolbar (required)
|
|
664
|
+
{
|
|
665
|
+
"point": "toolbar",
|
|
666
|
+
"icon": "assets/icon.svg",
|
|
667
|
+
"label": { "ko": "내 플러그인", "en": "My Plugin" },
|
|
668
|
+
"ui": "content", // must be unique across all contributions
|
|
669
|
+
"opens": "popover" // popover | panel | page
|
|
670
|
+
},
|
|
671
|
+
// move.panel — adds a panel to the Move screen
|
|
672
|
+
{
|
|
673
|
+
"point": "move.panel",
|
|
674
|
+
"icon": "assets/icon.svg",
|
|
675
|
+
"label": { "ko": "내 플러그인", "en": "My Plugin" },
|
|
676
|
+
"ui": "move" // must be unique across all contributions
|
|
677
|
+
}
|
|
678
|
+
],
|
|
679
|
+
|
|
680
|
+
// optional: variables this plugin exposes to pyfm programs — the Program
|
|
681
|
+
// page variable picker lists ONLY declared variables. name must start with
|
|
682
|
+
// RBP_; description is optional but without it the UI can't explain the
|
|
683
|
+
// variable, so write one.
|
|
684
|
+
"variables": [
|
|
685
|
+
{ "name": "RBP_grip_force", "type": "number", "init": 0.5, "description": { "ko": "그리퍼 힘", "en": "Gripper force" } },
|
|
686
|
+
{ "name": "RBP_target_pose", "type": "array", "init": [0, 0, 0] }
|
|
687
|
+
],
|
|
688
|
+
|
|
689
|
+
// runtime — one of:
|
|
690
|
+
// { "type": "none" }
|
|
691
|
+
// { "type": "python", "entry": "main.py" }
|
|
692
|
+
// { "type": "cpp", "entry": "<executable name>" }
|
|
693
|
+
// { "type": "ros2", "rosClient": "rclpy" | "rclcpp", "package": "...", "launch": "..._node" }
|
|
694
|
+
"runtime": { "type": "cpp", "entry": "__PLUGIN_SLUG__" },
|
|
695
|
+
|
|
696
|
+
// optional: TCP ports opened by this plugin backend
|
|
697
|
+
// only number[] is allowed; ranges/objects are not accepted
|
|
698
|
+
// external plugin ports must be within 28600-28999
|
|
699
|
+
"ports": [28600]
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
**Backend types:**
|
|
704
|
+
|
|
705
|
+
| type | source dir | build / run | entry |
|
|
706
|
+
|---|---|---|---|
|
|
707
|
+
| `python` | `py/` | pip + run in Docker | `main.py` |
|
|
708
|
+
| `cpp` | `cpp/` | CMake in Docker | built executable name |
|
|
709
|
+
| `ros2` (rclpy) | `ros2_ws/` | colcon (ament_python) in Docker | — |
|
|
710
|
+
| `ros2` (rclcpp) | `ros2_ws/` | colcon (ament_cmake) in Docker | — |
|
|
711
|
+
|
|
712
|
+
> 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.
|
|
713
|
+
|
|
714
|
+
**Contribution points:**
|
|
715
|
+
|
|
716
|
+
| point | Where it appears | Backend | `commandInfo` |
|
|
717
|
+
|---|---|---|:---:|
|
|
718
|
+
| `toolbar` | Top toolbar button (must have at least one) | SDK / `rb.action` | — |
|
|
719
|
+
| `lnb` | Left navigation entry | SDK / `rb.action` | — |
|
|
720
|
+
| `move.panel` | Move screen panel | SDK / `rb.action` | — |
|
|
721
|
+
| `program.panel` | Custom command in the Program page sequence builder | `PluginProgramContext` | ✓ |
|
|
722
|
+
|
|
723
|
+
**`commandInfo` fields** (for `program.panel`):
|
|
724
|
+
|
|
725
|
+
| field | type | description |
|
|
726
|
+
|---|---|---|
|
|
727
|
+
| `name` | `string` | Display name of the command in the program tree |
|
|
728
|
+
| `icon` | `string` | Icon path |
|
|
729
|
+
| `iconClassName` | `string?` | Optional CSS class for the icon |
|
|
730
|
+
| `funcName` | `string?` | Backend function name to call |
|
|
731
|
+
| `args` | `Record<string, any>?` | Default arguments |
|
|
732
|
+
| `disabled` | `boolean?` | Whether the node is disabled |
|
|
733
|
+
| `allowChildren` | `string[]?` | Allowed child commands |
|
|
734
|
+
| `allowParents` | `string[]?` | Allowed parent commands |
|
|
735
|
+
| `notAllowChildren` | `string[]?` | Disallowed child commands |
|
|
736
|
+
| `syncGroupId` | `boolean?` | Sync group ID across children |
|
|
737
|
+
| `disableCopy` | `boolean?` | Prevent copy |
|
|
738
|
+
| `disableDelete` | `boolean?` | Prevent delete |
|
|
739
|
+
| `requireChildren` | `boolean?` | Whether a child command is required |
|
|
740
|
+
| `requireParent` | `boolean?` | Whether a parent command is required |
|
|
741
|
+
| `childrenCommand` | `string?` | Default command type for children |
|
|
742
|
+
|
|
743
|
+
**Validation rules:**
|
|
744
|
+
- `id`, `name`, `version`, `minMuscatVersion` — required non-empty strings
|
|
745
|
+
- `author` — required non-empty string
|
|
746
|
+
- `description` — required (`string` or `{ en (required), ko?, ja?, zh?, vi?, th?, es? }`)
|
|
747
|
+
- `icons` — SVG: single string path ending in `.svg`; PNG/JPG: object with `"16"`, `"48"`, `"128"` all present
|
|
748
|
+
- `targetModels` — `"all"` or a non-empty array of robot model ID strings (at least one of `targetModels` / `targetCategorys` required)
|
|
749
|
+
- `targetCategorys` — `"all"` or a non-empty array of `"manipulate" | "amr" | "rby1" | "vision"` (at least one of `targetModels` / `targetCategorys` required)
|
|
750
|
+
- `contributes` — required array; must contain at least one `toolbar` entry
|
|
751
|
+
- `ui` — each contribution must have a unique value
|
|
752
|
+
- `commandInfo` — required when `point` is `"program.panel"`
|
|
753
|
+
- `ports` — optional `number[]`; every port must be within `28600-28999`
|
|
754
|
+
and must not be a reserved robot/industrial/internal port
|
|
755
|
+
- `variables` — optional array of `{ name, type, init, description? }`:
|
|
756
|
+
- `name` — must start with `RBP_`, then letters/digits/underscore; unique per plugin
|
|
757
|
+
- `type` — `"number" | "string" | "array"`
|
|
758
|
+
- `init` — required initial value, must match `type`
|
|
759
|
+
- `description` — optional `string` or i18n object (`en` required in object form)
|
|
760
|
+
|
|
761
|
+
`muscat-pk build` throws on any validation error. `muscat-pk dev` prints
|
|
762
|
+
warnings — except `variables` errors (e.g. a name without the `RBP_` prefix),
|
|
763
|
+
which are hard errors in `dev` / `dev:back` too.
|
|
764
|
+
|
|
765
|
+
**Backend port rules:**
|
|
766
|
+
- Declare a port only if your backend opens a TCP server/listener.
|
|
767
|
+
- **Every listening TCP port must be declared.** `muscat-pk dev` and
|
|
768
|
+
`muscat-pk dev:back` watch the backend and stop it the moment it opens a port
|
|
769
|
+
that is not in `ports`. A port outside `28600-28999` can't be declared, so it
|
|
770
|
+
must not be opened either — picking e.g. `5500` is not a way around the rule.
|
|
771
|
+
It would escape this check *and* the robot's install-time conflict check, and
|
|
772
|
+
only collide once it's running on a robot.
|
|
773
|
+
- Reserved ports include `502`, `2222`, `4840`, `5170-5179`, `8000-8999`,
|
|
774
|
+
`10000`, `30004`, `34964`, `44818`, `50000-50005`, and `50051`.
|
|
775
|
+
- The robot rejects installation (HTTP 409) if a declared port is reserved,
|
|
776
|
+
out of range, or already claimed by another enabled plugin.
|
|
777
|
+
|
|
778
|
+
**Reaching your backend's ports during development:**
|
|
779
|
+
- Declared ports are published to `127.0.0.1` on your machine, so
|
|
780
|
+
`curl http://127.0.0.1:28600` works from the host on macOS, Windows, and Linux.
|
|
781
|
+
- On native Linux the dev container uses host networking, so the ports are on
|
|
782
|
+
the host already.
|
|
783
|
+
- If a declared port is already taken on your machine, `muscat-pk dev` warns and
|
|
784
|
+
skips publishing that one — the backend still listens inside the container.
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
## Localized documentation
|
|
789
|
+
|
|
790
|
+
You can ship per-language README files alongside the main `README.md`. Place them in the project root:
|
|
791
|
+
|
|
792
|
+
```
|
|
793
|
+
README.md # default / English fallback
|
|
794
|
+
README.ko.md # Korean
|
|
795
|
+
README.en.md # explicit English (optional)
|
|
796
|
+
README.ja.md # Japanese
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
All `README*.md` files found at the project root are automatically copied to `dist/` on
|
|
800
|
+
`muscat-pk build`. Muscat displays the file matching the user's locale,
|
|
801
|
+
falling back to `README.md` if none matches.
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
## C++ SDK & dependencies
|
|
806
|
+
|
|
807
|
+
The dev container has the C++ SDK (`rainbow-rb-sdk-dev`) pre-installed — no local
|
|
808
|
+
install needed. Pick a different SDK version with:
|
|
809
|
+
|
|
810
|
+
```bash
|
|
811
|
+
MUSCAT_PK_RB_SDK_VERSION=<version> MUSCAT_PK_REBUILD_IMAGE=1 muscat-pk dev
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
`muscat-pk build` copies the matching `librb_sdk.so` next to your binary
|
|
815
|
+
(`dist/<arch>/lib/`, or `install-<arch>/<pkg>/lib/` for rclcpp), so the robot needs no SDK
|
|
816
|
+
install and each plugin keeps the SDK version it was built with.
|
|
817
|
+
|
|
818
|
+
Other libraries: install them in the build (e.g. vendor them under `cpp/` and
|
|
819
|
+
`add_subdirectory`, or link statically) — the robot only has Ubuntu 24.04 base
|
|
820
|
+
libraries.
|
|
821
|
+
|
|
822
|
+
To use the SDK outside a plugin (your own Ubuntu 24.04 machine):
|
|
823
|
+
|
|
824
|
+
```bash
|
|
825
|
+
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
|
|
826
|
+
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
|
|
827
|
+
sudo apt update && sudo apt install rainbow-rb-sdk-dev
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
### Bundling extra shared libraries
|
|
832
|
+
|
|
833
|
+
The robot only has Ubuntu 24.04 base libraries. Copy any other `.so` your executable
|
|
834
|
+
needs into `${CMAKE_BINARY_DIR}/lib` — `muscat-pk build` ships that folder as
|
|
835
|
+
`dist/<arch>/lib/` and the executable finds it via its RPATH (`$ORIGIN/lib`):
|
|
836
|
+
|
|
837
|
+
```cmake
|
|
838
|
+
# cpp/CMakeLists.txt — third_party/ holds prebuilt libs for amd64 / arm64
|
|
839
|
+
set(ARCH_DIR ${CMAKE_SOURCE_DIR}/third_party/${CMAKE_SYSTEM_PROCESSOR})
|
|
840
|
+
target_link_libraries(__PLUGIN_SLUG__ PRIVATE ${ARCH_DIR}/libfoo.so)
|
|
841
|
+
add_custom_command(TARGET __PLUGIN_SLUG__ POST_BUILD
|
|
842
|
+
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/lib
|
|
843
|
+
COMMAND ${CMAKE_COMMAND} -E copy ${ARCH_DIR}/libfoo.so ${CMAKE_BINARY_DIR}/lib/)
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
Build them for both arches (`x86_64`, `aarch64`) on Ubuntu 24.04 so they match the robot.
|
|
847
|
+
Static linking works too and needs no bundling. For an rclcpp node, `install(FILES ... DESTINATION lib)`
|
|
848
|
+
instead — the node finds `install-<arch>/<pkg>/lib/` the same way.
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## Switching backend type
|
|
852
|
+
|
|
853
|
+
```bash
|
|
854
|
+
muscat-pk type change # interactive
|
|
855
|
+
muscat-pk type change --to cpp # → C++
|
|
856
|
+
muscat-pk type change --to ros2 # → ROS2 (rclpy / Python)
|
|
857
|
+
muscat-pk type change --to ros2-cpp # → ROS2 (rclcpp / C++)
|
|
858
|
+
muscat-pk type change --to python # → Python
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
> Existing backend dirs (`ros2_ws/`, `cpp/`) are not deleted when switching away. Remove them manually if no longer needed.
|
|
862
|
+
|
|
863
|
+
> Switching between a Python and a C++ backend asks whether to replace this README with the matching one (the old file is kept as `README.md.bak`). Add `--readme` to answer yes without the prompt.
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
## AI coding assistants (MCP)
|
|
868
|
+
|
|
869
|
+
If you build your plugin with an AI assistant (Claude Code, Codex, …), give it
|
|
870
|
+
grounded knowledge of the Muscat UI components, design tokens, and SDK instead of
|
|
871
|
+
letting it guess. `muscat-pk mcp` is an MCP server exposing:
|
|
872
|
+
|
|
873
|
+
- `list_components` / `get_component_props` / `get_component_example` — every
|
|
874
|
+
`@rainbow-robotics/plugin-ui/react` component with its real props and a working
|
|
875
|
+
Storybook example.
|
|
876
|
+
- `list_design_tokens` / `search_design_tokens` — the Tailwind classes to use
|
|
877
|
+
(`bg-bg-*`, `text-fg-*`, `typo-*`, …) so your UI matches the host theme.
|
|
878
|
+
- `search_rb_sdk` — how to use `useRb()`, `rb.*`, `useRbFormData`/`useRbSave`, and
|
|
879
|
+
the backend runtimes.
|
|
880
|
+
- `list_rb_sdk` / `get_rb_sdk_api` — the installed `rainbow-rb-sdk` (Python
|
|
881
|
+
backend) classes and signatures; with `lang: "cpp"`, the C++ SDK headers
|
|
882
|
+
(`rainbow-rb-sdk-dev`, read out of the `muscat-pk` cpp dev image — build it
|
|
883
|
+
once with `muscat-pk dev:back`).
|
|
884
|
+
|
|
885
|
+
**Browse the components live** with `muscat-pk storybook`.
|
|
886
|
+
|
|
887
|
+
### Register the server
|
|
888
|
+
|
|
889
|
+
**Claude Code** — a `.mcp.json` is already scaffolded at your plugin root, so
|
|
890
|
+
it works out of the box:
|
|
891
|
+
|
|
892
|
+
```json
|
|
893
|
+
{
|
|
894
|
+
"mcpServers": {
|
|
895
|
+
"muscat": {
|
|
896
|
+
"command": "muscat-pk",
|
|
897
|
+
"args": ["mcp"]
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
**Codex** — register once with:
|
|
904
|
+
|
|
905
|
+
```bash
|
|
906
|
+
muscat-pk mcp --install codex
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
This appends the server to `~/.codex/config.toml` (idempotent — safe to run
|
|
910
|
+
again). Equivalent manual config:
|
|
911
|
+
|
|
912
|
+
```toml
|
|
913
|
+
[mcp_servers.muscat]
|
|
914
|
+
command = "muscat-pk"
|
|
915
|
+
args = ["mcp"]
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
Any other MCP client works too — it's a standard stdio MCP server started with
|
|
919
|
+
`muscat-pk mcp`. If `muscat-pk` isn't on the client's PATH, use its absolute path
|
|
920
|
+
(`which muscat-pk`) as `command`.
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
924
|
+
## Shipping
|
|
925
|
+
|
|
926
|
+
```bash
|
|
927
|
+
muscat-pk build # build + ZIP
|
|
928
|
+
muscat-pk build --no-zip # build only, no ZIP
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
- Builds `ui/content.tsx` → `dist/content.js` (minified)
|
|
932
|
+
- Compiles `cpp/` (Release) in the Ubuntu 24.04 container per arch → `dist/<arch>/<entry>` + `dist/<arch>/lib/librb_sdk.so.*`
|
|
933
|
+
- ROS2 (rclcpp): runs `colcon build` per arch → `dist/ros2_ws/install-<arch>/`
|
|
934
|
+
- Copies `assets/`, `manifest.json`, and `README*.md` into `dist/`
|
|
935
|
+
- Output: `__PLUGIN_DIR__-{version}.zip`
|
|
936
|
+
|
|
937
|
+
ZIP layout:
|
|
938
|
+
|
|
939
|
+
```
|
|
940
|
+
__PLUGIN_DIR__-1.0.0.zip
|
|
941
|
+
├─ dist/
|
|
942
|
+
│ ├─ content.js # built UI
|
|
943
|
+
│ ├─ manifest.json
|
|
944
|
+
│ ├─ assets/
|
|
945
|
+
│ ├─ README.md
|
|
946
|
+
│ ├─ README.ko.md # if present
|
|
947
|
+
│ ├─ amd64/ # cpp: <entry> + lib/librb_sdk.so.*
|
|
948
|
+
│ ├─ arm64/
|
|
949
|
+
│ └─ ros2_ws/ # rclcpp: install-amd64/, install-arm64/
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
## All commands
|
|
955
|
+
|
|
956
|
+
```bash
|
|
957
|
+
muscat-pk dev # UI + backend
|
|
958
|
+
muscat-pk dev:ui # UI preview only (preview-host)
|
|
959
|
+
muscat-pk dev:back # backend only (C++ rebuild + restart in Docker)
|
|
960
|
+
muscat-pk build # build → dist/ + distributable ZIP (--no-zip to skip)
|
|
961
|
+
muscat-pk storybook # browse the rb-components UI library in a browser
|
|
962
|
+
muscat-pk mcp # MCP server for AI assistants (components/tokens/SDK docs)
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
```bash
|
|
966
|
+
muscat-pk type change # switch backend type
|
|
967
|
+
muscat-pk update # update muscat-plugin-kit itself
|
|
968
|
+
muscat-pk uninstall # remove muscat-pk itself
|
|
969
|
+
```
|