@rainbow-robotics/muscat-plugin-kit 1.3.7 → 1.3.8-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/cli/README.md +11 -5
- package/cli/bin/muscat-pk.mjs +1 -1
- package/cli/bundle-templates.mjs +0 -2
- package/cli/docker/Dockerfile.cpp +6 -22
- package/cli/docker/Dockerfile.dev +6 -22
- package/cli/docker/Dockerfile.python +0 -18
- package/cli/src/build.mjs +5 -5
- package/cli/src/completion.mjs +1 -1
- package/cli/src/cpp.mjs +11 -3
- package/cli/src/cppIde.mjs +75 -0
- package/cli/src/dev.mjs +19 -26
- package/cli/src/devBack.mjs +29 -22
- package/cli/src/devUi.mjs +10 -1
- package/cli/src/muscatLocal.mjs +198 -0
- package/cli/src/new.mjs +15 -0
- package/cli/src/ports.mjs +1 -1
- package/cli/src/ros2.mjs +11 -3
- package/cli/src/sdk.mjs +48 -2
- package/cli/src/type.mjs +2 -0
- package/cli/src/util.mjs +46 -3
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-CdTgUqrV.js → Color-YHDXOIA2-DeirXVfl.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CbRKwHpQ.js → Docs-BLxyi8cP.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CsxMprX5.js → Docs-Buyf7Djy.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-FyPFrbUS.js → Docs-BxmwdaYa.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-C8wx5zsC.js → Docs-CnfIOwf8.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-Cf0s04QW.js → Docs-NrBqEYvM.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-YtqU-aVu.js → Docs-wzFMK-XA.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-VSENNRoY.js → DocsRenderer-CFRXHY34-CWc43rTP.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-oCcYtLwq.js → NxAvatar.stories-DfL0kqmN.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-C5jUScIj.js → NxButton.stories-WxSc_gsR.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-gjsEulIN.js → NxNumberInput.stories-C1D3oUiY.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-BK6uSWIa.js → NxPagination.stories-C1Qa88rn.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BDD5CvSZ.js → NxPointerHoldButton.stories-CmteJX3t.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-C_nW26_x.js → NxRangeInput.stories-B7E-mhyI.js} +1 -1
- package/cli/storybook-dist/assets/{NxSelectBox.stories-DqLfKU9H.js → NxSelectBox.stories-De67vMdU.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CQjHA8yv.js → NxSlideRange.stories-DvCV1MGk.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-BMHCiPoL.js → NxSwitch.stories-DuHDZ9-v.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-k7r1WVQh.js → NxTextArea.stories-BT1o0ngg.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-Cqfe1I32.js → NxTextInput.stories-CSLBT83Q.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-CJ3Ttuja.js → RBAccordion.stories-QavkYgV0.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-BThv7tM0.js → RBAccordionGroup.stories-C0RPGnO7.js} +1 -1
- package/cli/storybook-dist/assets/{RBAlert.stories-CSMP8Vun.js → RBAlert.stories-BUCAT0Sd.js} +1 -1
- package/cli/storybook-dist/assets/{RBBadge.stories-cBMA2R6F.js → RBBadge.stories-CnBXTQ4J.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-BKVOskqs.js → RBBattery.stories-5FbsZcdy.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-BTOfK0HC.js → RBCard.stories-FwTKjtCi.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckBox.stories-DyE912wG.js → RBCheckBox.stories-C0y5psMI.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-0sLFU0kM.js → RBCheckIconButton.stories-sJjAZBVh.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-c8VecClE.js → RBCheckLabelButton.stories-COJqEkDr.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipTab.stories-CQ9Uav5_.js → RBChipTab.stories-BnXj-Woa.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-Ap4NlhAZ.js → RBChipsBoxTab.stories-DE1yl3rz.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-BPPXjif2.js → RBColorIcons.stories-CYASERLQ.js} +1 -1
- package/cli/storybook-dist/assets/{RBCommand.stories-PDpYOKlm.js → RBCommand.stories-BYkWjdMG.js} +1 -1
- package/cli/storybook-dist/assets/{RBConfirm.stories-BEWvMAYP.js → RBConfirm.stories-XLLTJZRf.js} +1 -1
- package/cli/storybook-dist/assets/{RBDateInput.stories-DLdyNnVF.js → RBDateInput.stories-DpVylrQl.js} +1 -1
- package/cli/storybook-dist/assets/{RBDialog.stories-HsawMNOb.js → RBDialog.stories-DcPRAan8.js} +1 -1
- package/cli/storybook-dist/assets/{RBDivider.stories-dlWwOxik.js → RBDivider.stories-NOTo-jJG.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-C87iqGk0.js → RBDrawer.stories-CUZaQyw8.js} +1 -1
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-B8EQhA0z.js → RBDropdownMenu.stories-5UC0ZA3H.js} +1 -1
- package/cli/storybook-dist/assets/{RBEmptyView.stories-SzpIhNDw.js → RBEmptyView.stories-BIRNPjwt.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-BQ0nM1gb.js → RBIconButton.stories-CJWeIQTt.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-C19g5M5z.js → RBIcons.stories-Cu0txCUp.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-DbgI4-1P.js → RBInlineLabelButton.stories-DnGhYrxa.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-smF-vpGL.js → RBJoyStick.stories-BlfvKcGb.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-s53klwDv.js → RBLabelButton.stories-CFwmlhAE.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-BnzkoR51.js → RBLight.stories-wMrFLJ7u.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-CW014klu.js → RBMenubar.stories-Bu5wtV-T.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-qa4tH_4Y.js → RBMultiRangeInput.stories-Db1d940z.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-YoQNUpQH.js → RBPagination.stories-NCpjg-2W.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-Bhpi0OaH.js → RBPopover.stories-CqsM8byU.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-iQM5iAka.js → RBPrompt.stories-TMQEaHk-.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-BGyq4rzx.js → RBRadioBox.stories-pnsBvtzU.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-DF6BcUcJ.js → RBRadioButton.stories-BMDBfq82.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-COOQ_HNF.js → RBRangeDateInput.stories-ljAsdz0Q.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-DSVHvwjp.js → RBResizable.stories-B5YDW687.js} +1 -1
- package/cli/storybook-dist/assets/{RBSelectBox.stories-BookV8Sm.js → RBSelectBox.stories-DPtyIwEr.js} +1 -1
- package/cli/storybook-dist/assets/{RBSheet.stories-CAK9hb-p.js → RBSheet.stories-DMTeF_hx.js} +1 -1
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DUymnH-9.js → RBSideNumberInput.stories-DLsS3mzL.js} +1 -1
- package/cli/storybook-dist/assets/{RBSlider.stories-BwlDh9--.js → RBSlider.stories-BjTYZ3ri.js} +1 -1
- package/cli/storybook-dist/assets/{RBSpinner.stories-VOlkuP6t.js → RBSpinner.stories-BWBvOGUY.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-BiW2dQnT.js → RBSwitch.stories-tpthe98k.js} +1 -1
- package/cli/storybook-dist/assets/{RBTab.stories-DFKVsy0C.js → RBTab.stories-BijmaLIo.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-BWG0ZSYI.js → RBTable.stories-Dojsz28q.js} +1 -1
- package/cli/storybook-dist/assets/{RBTableRow.stories-JVTD83Mh.js → RBTableRow.stories-CQajMiEa.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-D-aN385u.js → RBTabs.stories-BslRXoCw.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextArea.stories-Dx5xkmZS.js → RBTextArea.stories-N44Q45Xp.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextField.stories-DmVciExd.js → RBTextField.stories-scpiy0Kx.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextInput.stories-CCf0VGEv.js → RBTextInput.stories-D_suHJaA.js} +1 -1
- package/cli/storybook-dist/assets/{RBToast.stories-DT0DGn05.js → RBToast.stories-B73JQcKo.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-Ce_q3EtP.js → RBToggle.stories-CmujM833.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-CN40YHsR.js → RBToggleGroup.stories-DhCAfiKu.js} +1 -1
- package/cli/storybook-dist/assets/{RBTooltip.stories-L7btDEpA.js → RBTooltip.stories-BixEjKZy.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-B5_xLK8z.js → SideNumberInput-B7fbx2jk.js} +83 -83
- package/cli/storybook-dist/assets/{entry-preview-raBQdKjS.js → entry-preview-BeR5Td7C.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-BmIue__l.js → iframe-D3pKf_6w.js} +2 -2
- package/cli/storybook-dist/assets/{index-CSIe-YDr.js → index-gXlU0WyC.js} +3 -3
- package/cli/storybook-dist/assets/preview-BYhMD55w.css +1 -0
- package/cli/storybook-dist/assets/preview-D-VpQB0_.js +3 -0
- package/cli/storybook-dist/assets/{preview-DD51RTKv.js → preview-GwaHbm9r.js} +2 -2
- package/cli/storybook-dist/assets/react-resizable-panels-DJMuB2U0.css +1 -0
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/README.md +10 -22
- package/cli/templates/common/requirements.txt +3 -3
- package/cli/templates/cpp/cpp/CMakeLists.txt +3 -2
- package/cli/templates/readme-cpp/README.md +18 -23
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +3 -2
- package/package.json +3 -4
- package/cli/docker/Dockerfile.bridge +0 -17
- package/cli/docker/bridge-package.json +0 -8
- package/cli/src/bridge.mjs +0 -152
- package/cli/src/socketBridge.mjs +0 -133
- package/cli/storybook-dist/assets/preview-CUgthyJT.js +0 -3
- package/cli/storybook-dist/assets/preview-zuAvMksL.css +0 -1
- package/cli/storybook-dist/assets/react-resizable-panels-BaelaOGy.css +0 -1
- /package/cli/storybook-dist/assets/{react-resizable-panels.browser-CYcsrUgg.js → react-resizable-panels.browser-D49JKwKo.js} +0 -0
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ muscat-pk build # build dist/ and produce a distributable ZIP (sk
|
|
|
115
115
|
| `muscat-pk storybook` | Browse the bundled rb-components Storybook |
|
|
116
116
|
| `muscat-pk mcp` | Run an MCP server for AI coding agents (exposes rb-components/design-token/SDK docs) |
|
|
117
117
|
| `muscat-pk update` | Update the kit + plugin-ui to the latest version |
|
|
118
|
-
| `muscat-pk sdk <list\|update>` | Manage the
|
|
118
|
+
| `muscat-pk sdk <list\|update>` | Manage the `rainbow-rb-sdk` version — python: `requirements.txt`, C++ (cpp / ros2-cpp): `find_package(rb_sdk <ver> EXACT REQUIRED)` in `CMakeLists.txt` |
|
|
119
119
|
| `muscat-pk version` | Print the installed CLI version (`-v` / `--version` also work) |
|
|
120
120
|
| `muscat-pk completion [shell]` | Print a shell completion script (`bash` / `zsh` / `fish` / `powershell`; defaults to `$SHELL`) |
|
|
121
121
|
|
|
@@ -159,7 +159,7 @@ Move images to a machine with no internet:
|
|
|
159
159
|
|
|
160
160
|
```bash
|
|
161
161
|
# on a machine with network
|
|
162
|
-
docker save muscat-pk-python-dev muscat-pk-
|
|
162
|
+
docker save muscat-pk-python-dev muscat-pk-runtime | gzip > muscat-pk-images.tar.gz
|
|
163
163
|
|
|
164
164
|
# on the offline machine
|
|
165
165
|
gunzip -c muscat-pk-images.tar.gz | docker load
|
|
@@ -198,12 +198,12 @@ The runtime monitor is strict on purpose: an undeclared port outside the plugin
|
|
|
198
198
|
range is invisible to both the monitor and the robot's conflict check, so it
|
|
199
199
|
would only surface as a collision after deployment.
|
|
200
200
|
|
|
201
|
-
`muscat-pk`'s own dev plumbing (the
|
|
201
|
+
`muscat-pk`'s own dev plumbing (the plugin gateway on `10000`, the Zenoh dev
|
|
202
202
|
peer port) is excluded from the check — it isn't the plugin's traffic.
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
Under `muscat-pk dev` declared ports are published to `127.0.0.1` from the local
|
|
205
|
+
Muscat container (the backend shares its network), so you can reach the backend
|
|
206
|
+
from the host.
|
|
207
207
|
|
|
208
208
|
## Variables (pyfm)
|
|
209
209
|
|
package/cli/README.md
CHANGED
|
@@ -42,12 +42,12 @@ the backend**, whatever its number. Since a port outside `28600-28999` cannot be
|
|
|
42
42
|
declared, it cannot be opened either — otherwise it would slip past both this
|
|
43
43
|
check and the robot's install-time conflict check.
|
|
44
44
|
|
|
45
|
-
The kit's own dev ports (
|
|
45
|
+
The kit's own dev ports (plugin gateway `10000`, the Zenoh dev peer port) are
|
|
46
46
|
excluded from that check.
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
Under `muscat-pk dev` the backend shares the local Muscat container's network, so
|
|
49
|
+
declared ports are published to `127.0.0.1` from that container. A declared port
|
|
50
|
+
that is already busy on the host is skipped with a warning.
|
|
51
51
|
|
|
52
52
|
Known robot/industrial/internal ports such as `502`, `4840`, `5170-5179`,
|
|
53
53
|
`8000-8999`, `10000`, `2222`, `30004`, `34964`, `44818`, `50000-50005`,
|
|
@@ -226,7 +226,13 @@ inside plugins.
|
|
|
226
226
|
- **`build`** bundles plugin code into ESM while externalizing
|
|
227
227
|
`@rainbow-robotics/plugin-ui`, React, and ReactDOM.
|
|
228
228
|
- **`dev:ui`** builds the plugin then serves it inside the bundled preview host
|
|
229
|
-
at `/plugin-dev` — a real mini-Muscat shell
|
|
229
|
+
at `/plugin-dev` — a real mini-Muscat shell. Without a robot IP it needs a Muscat
|
|
230
|
+
on `localhost:3000` for program preview: `dev` / `dev:ui` reuse any container that
|
|
231
|
+
already publishes `:10000` (`muscat`, `muscat-dev`, …) and otherwise start
|
|
232
|
+
`muscat-pk-muscat` — the S3 runtime image (`muscat/main/muscat.zip`) running only the
|
|
233
|
+
`common` service from `muscat/main/version.json`, cached under `~/.muscat-pk/muscat/`.
|
|
234
|
+
It keeps running between runs (`docker rm -f muscat-pk-muscat` to stop);
|
|
235
|
+
`MUSCAT_PK_NO_LOCAL_MUSCAT=1` disables it.
|
|
230
236
|
|
|
231
237
|
`create-muscat-plugin <dir>` is the bin alias for `muscat-pk new`.
|
|
232
238
|
|
package/cli/bin/muscat-pk.mjs
CHANGED
|
@@ -81,7 +81,7 @@ function formatHelp() {
|
|
|
81
81
|
commandRow("new [dir]", `Scaffold a new plugin project ${color.dim("(prompts: author, email, description, type, front-package-manager)")}`),
|
|
82
82
|
commandRow("type change", `Switch backend type ${color.dim("(python | cpp | ros2-python | ros2-cpp)")}`),
|
|
83
83
|
commandRow("build", `Build + lock deps + vendor wheels + zip ${color.dim(`(${option("--no-zip")}: skip zip, ${option("--no-lock")}: skip python lock/vendor, ${option("--arch")} amd64|arm64|both)`)}`),
|
|
84
|
-
commandRow("dev", "Run
|
|
84
|
+
commandRow("dev", "Run a local Muscat runtime, dev:ui, and dev:back together"),
|
|
85
85
|
commandRow("dev:ui", `Preview the plugin in a mini-Muscat ${color.dim("(preview-host)")}`),
|
|
86
86
|
commandRow("dev:back", `Run the plugin back end ${color.dim("(python: watchfiles reload / ros2: Docker)")}`),
|
|
87
87
|
commandRow("storybook", `Browse the bundled rb-components Storybook ${color.dim(`(${option("--port")} <n>, ${option("--rebuild")})`)}`),
|
package/cli/bundle-templates.mjs
CHANGED
|
@@ -19,8 +19,6 @@ function walk(baseDir, prefix) {
|
|
|
19
19
|
}
|
|
20
20
|
walk(resolve(root, "templates") + "/", "");
|
|
21
21
|
walk(resolve(root, "docker") + "/", "docker/");
|
|
22
|
-
// Dockerfile.bridge 의 COPY 대상
|
|
23
|
-
files["src/socketBridge.mjs"] = readFileSync(resolve(root, "src/socketBridge.mjs"), "base64");
|
|
24
22
|
|
|
25
23
|
const { version } = JSON.parse(readFileSync(resolve(root, "../package.json"), "utf8"));
|
|
26
24
|
|
|
@@ -10,34 +10,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
10
10
|
cmake \
|
|
11
11
|
&& rm -rf /var/lib/apt/lists/*
|
|
12
12
|
|
|
13
|
-
# Rainbow Robotics C++ SDK for find_package(rb_sdk).
|
|
14
|
-
#
|
|
15
|
-
|
|
13
|
+
# Rainbow Robotics C++ SDK for find_package(rb_sdk). muscat-pk passes the version pinned in the
|
|
14
|
+
# project's CMakeLists.txt (find_package(rb_sdk <ver> EXACT)); without a pin (ROS 2 rclpy, `muscat-pk mcp`
|
|
15
|
+
# outside a project) the newest SDK is installed. Runtime and -dev are installed together at one version.
|
|
16
|
+
ARG RB_SDK_VERSION=
|
|
16
17
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
|
|
17
18
|
&& install -d /etc/apt/keyrings \
|
|
18
19
|
&& curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
|
|
19
20
|
&& printf 'deb [signed-by=/etc/apt/keyrings/rb-sdk.asc] https://rainbowrobotics.github.io/rb-sdk %s main\n' stable dev > /etc/apt/sources.list.d/rb-sdk.list \
|
|
20
|
-
&& apt-get update
|
|
21
|
+
&& apt-get update \
|
|
22
|
+
&& apt-get install -y --no-install-recommends "rainbow-rb-sdk${RB_SDK_VERSION:+=$RB_SDK_VERSION}" "rainbow-rb-sdk-dev${RB_SDK_VERSION:+=$RB_SDK_VERSION}" \
|
|
21
23
|
&& rm -rf /var/lib/apt/lists/*
|
|
22
24
|
|
|
23
|
-
# MediaMTX — same version/ports as the Muscat runtime (rby2 scripts/backend/runtime/mediamtx.yml),
|
|
24
|
-
# plus webrtcAdditionalHosts so a browser on the dev host reaches the WebRTC media port.
|
|
25
|
-
ARG MEDIAMTX_VERSION=1.12.3
|
|
26
|
-
RUN set -eux; \
|
|
27
|
-
apt-get update; apt-get install -y --no-install-recommends ca-certificates curl; rm -rf /var/lib/apt/lists/*; \
|
|
28
|
-
case "$(uname -m)" in x86_64) a=amd64 ;; aarch64|arm64) a=arm64 ;; *) exit 1 ;; esac; \
|
|
29
|
-
curl -fsSL "https://github.com/bluenviron/mediamtx/releases/download/v${MEDIAMTX_VERSION}/mediamtx_v${MEDIAMTX_VERSION}_linux_${a}.tar.gz" \
|
|
30
|
-
| tar -xz -C /usr/local/bin mediamtx; \
|
|
31
|
-
mkdir -p /etc/mediamtx; \
|
|
32
|
-
printf '%s\n' \
|
|
33
|
-
'logLevel: warn' \
|
|
34
|
-
'rtsp: yes' 'rtspAddress: :8554' \
|
|
35
|
-
'webrtc: yes' 'webrtcAddress: :8889' \
|
|
36
|
-
'webrtcLocalUDPAddress: :8189' 'webrtcLocalTCPAddress: :8189' \
|
|
37
|
-
'webrtcAdditionalHosts: [127.0.0.1]' 'webrtcAllowOrigin: "*"' \
|
|
38
|
-
'hls: no' 'rtmp: no' 'srt: no' 'api: no' \
|
|
39
|
-
'paths:' ' all_others:' ' source: publisher' \
|
|
40
|
-
> /etc/mediamtx/mediamtx.yml
|
|
41
25
|
|
|
42
26
|
WORKDIR /plugin
|
|
43
27
|
|
|
@@ -9,34 +9,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
9
9
|
python3-colcon-common-extensions \
|
|
10
10
|
&& rm -rf /var/lib/apt/lists/*
|
|
11
11
|
|
|
12
|
-
# Rainbow Robotics C++ SDK for find_package(rb_sdk).
|
|
13
|
-
#
|
|
14
|
-
|
|
12
|
+
# Rainbow Robotics C++ SDK for find_package(rb_sdk). muscat-pk passes the version pinned in the
|
|
13
|
+
# project's CMakeLists.txt (find_package(rb_sdk <ver> EXACT)); without a pin (ROS 2 rclpy, `muscat-pk mcp`
|
|
14
|
+
# outside a project) the newest SDK is installed. Runtime and -dev are installed together at one version.
|
|
15
|
+
ARG RB_SDK_VERSION=
|
|
15
16
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
|
|
16
17
|
&& install -d /etc/apt/keyrings \
|
|
17
18
|
&& curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
|
|
18
19
|
&& printf 'deb [signed-by=/etc/apt/keyrings/rb-sdk.asc] https://rainbowrobotics.github.io/rb-sdk %s main\n' stable dev > /etc/apt/sources.list.d/rb-sdk.list \
|
|
19
|
-
&& apt-get update
|
|
20
|
+
&& apt-get update \
|
|
21
|
+
&& apt-get install -y --no-install-recommends "rainbow-rb-sdk${RB_SDK_VERSION:+=$RB_SDK_VERSION}" "rainbow-rb-sdk-dev${RB_SDK_VERSION:+=$RB_SDK_VERSION}" \
|
|
20
22
|
&& rm -rf /var/lib/apt/lists/*
|
|
21
23
|
|
|
22
|
-
# MediaMTX — same version/ports as the Muscat runtime (rby2 scripts/backend/runtime/mediamtx.yml),
|
|
23
|
-
# plus webrtcAdditionalHosts so a browser on the dev host reaches the WebRTC media port.
|
|
24
|
-
ARG MEDIAMTX_VERSION=1.12.3
|
|
25
|
-
RUN set -eux; \
|
|
26
|
-
apt-get update; apt-get install -y --no-install-recommends ca-certificates curl; rm -rf /var/lib/apt/lists/*; \
|
|
27
|
-
case "$(uname -m)" in x86_64) a=amd64 ;; aarch64|arm64) a=arm64 ;; *) exit 1 ;; esac; \
|
|
28
|
-
curl -fsSL "https://github.com/bluenviron/mediamtx/releases/download/v${MEDIAMTX_VERSION}/mediamtx_v${MEDIAMTX_VERSION}_linux_${a}.tar.gz" \
|
|
29
|
-
| tar -xz -C /usr/local/bin mediamtx; \
|
|
30
|
-
mkdir -p /etc/mediamtx; \
|
|
31
|
-
printf '%s\n' \
|
|
32
|
-
'logLevel: warn' \
|
|
33
|
-
'rtsp: yes' 'rtspAddress: :8554' \
|
|
34
|
-
'webrtc: yes' 'webrtcAddress: :8889' \
|
|
35
|
-
'webrtcLocalUDPAddress: :8189' 'webrtcLocalTCPAddress: :8189' \
|
|
36
|
-
'webrtcAdditionalHosts: [127.0.0.1]' 'webrtcAllowOrigin: "*"' \
|
|
37
|
-
'hls: no' 'rtmp: no' 'srt: no' 'api: no' \
|
|
38
|
-
'paths:' ' all_others:' ' source: publisher' \
|
|
39
|
-
> /etc/mediamtx/mediamtx.yml
|
|
40
24
|
|
|
41
25
|
# `docker exec` skips /ros_entrypoint.sh — source ROS for the login shells dev:back runs.
|
|
42
26
|
RUN echo ". /opt/ros/${ROS_DISTRO}/setup.sh" > /etc/profile.d/ros.sh
|
|
@@ -15,24 +15,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
15
15
|
# cpp SDK shared lib must be present for the python module to load. Install the
|
|
16
16
|
# rainbow cpp SDK (apt, pinned to the robot's version) here once it exists.
|
|
17
17
|
|
|
18
|
-
# MediaMTX — same version/ports as the Muscat runtime (rby2 scripts/backend/runtime/mediamtx.yml),
|
|
19
|
-
# plus webrtcAdditionalHosts so a browser on the dev host reaches the WebRTC media port.
|
|
20
|
-
ARG MEDIAMTX_VERSION=1.12.3
|
|
21
|
-
RUN set -eux; \
|
|
22
|
-
apt-get update; apt-get install -y --no-install-recommends ca-certificates curl; rm -rf /var/lib/apt/lists/*; \
|
|
23
|
-
case "$(uname -m)" in x86_64) a=amd64 ;; aarch64|arm64) a=arm64 ;; *) exit 1 ;; esac; \
|
|
24
|
-
curl -fsSL "https://github.com/bluenviron/mediamtx/releases/download/v${MEDIAMTX_VERSION}/mediamtx_v${MEDIAMTX_VERSION}_linux_${a}.tar.gz" \
|
|
25
|
-
| tar -xz -C /usr/local/bin mediamtx; \
|
|
26
|
-
mkdir -p /etc/mediamtx; \
|
|
27
|
-
printf '%s\n' \
|
|
28
|
-
'logLevel: warn' \
|
|
29
|
-
'rtsp: yes' 'rtspAddress: :8554' \
|
|
30
|
-
'webrtc: yes' 'webrtcAddress: :8889' \
|
|
31
|
-
'webrtcLocalUDPAddress: :8189' 'webrtcLocalTCPAddress: :8189' \
|
|
32
|
-
'webrtcAdditionalHosts: [127.0.0.1]' 'webrtcAllowOrigin: "*"' \
|
|
33
|
-
'hls: no' 'rtmp: no' 'srt: no' 'api: no' \
|
|
34
|
-
'paths:' ' all_others:' ' source: publisher' \
|
|
35
|
-
> /etc/mediamtx/mediamtx.yml
|
|
36
18
|
|
|
37
19
|
WORKDIR /plugin
|
|
38
20
|
|
package/cli/src/build.mjs
CHANGED
|
@@ -353,7 +353,7 @@ function packZip(pluginRoot) {
|
|
|
353
353
|
|
|
354
354
|
/**
|
|
355
355
|
* Ask which target arch(es) to build for.
|
|
356
|
-
* Non-TTY (CI):
|
|
356
|
+
* Non-TTY (CI): both, same as the prompt default.
|
|
357
357
|
* @returns {Promise<string[]>} e.g. ['amd64'] | ['arm64'] | ['amd64','arm64']
|
|
358
358
|
*/
|
|
359
359
|
async function promptArch(args = []) {
|
|
@@ -364,11 +364,11 @@ async function promptArch(args = []) {
|
|
|
364
364
|
if (archFlag === "arm64" || archFlag === "aarch64") return ["arm64"];
|
|
365
365
|
throw new Error(`unknown --arch value '${archFlag}' (valid: amd64, arm64, both)`);
|
|
366
366
|
}
|
|
367
|
+
// Same default as the prompt: the target is the robot, not this machine
|
|
368
|
+
// (node's os.arch() is x64 under Rosetta on an arm64 Mac anyway).
|
|
367
369
|
if (!process.stdin.isTTY) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
log.info(`arch auto-selected (non-TTY): ${machine}`);
|
|
371
|
-
return [machine];
|
|
370
|
+
log.info("arch auto-selected (non-TTY): both");
|
|
371
|
+
return ["amd64", "arm64"];
|
|
372
372
|
}
|
|
373
373
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
374
374
|
process.stdout.write(
|
package/cli/src/completion.mjs
CHANGED
|
@@ -19,7 +19,7 @@ const COMMANDS = [
|
|
|
19
19
|
["mcp", "Start an MCP server (stdio)"],
|
|
20
20
|
["update", "Update muscat-pk and plugin-ui"],
|
|
21
21
|
["uninstall", "Remove the installed muscat-pk CLI"],
|
|
22
|
-
["sdk", "Manage the
|
|
22
|
+
["sdk", "Manage the rainbow-rb-sdk version (python: requirements.txt, C++: CMakeLists.txt)"],
|
|
23
23
|
["version", "Print the installed muscat-pk version"],
|
|
24
24
|
["completion", "Print a shell completion script"],
|
|
25
25
|
["help", "Show help"],
|
package/cli/src/cpp.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Used by dev:back (interactive dev loop) and build (export the binary into the ZIP).
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
|
-
import { canSkipImageBuild, imageKey, imageKeyLabel, dockerUserFlags, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
|
|
6
|
+
import { canSkipImageBuild, cppSdkVersion, pluginRootOrNull, imageKey, imageKeyLabel, dockerUserFlags, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
|
|
7
|
+
import { syncCppIde } from "./cppIde.mjs";
|
|
7
8
|
|
|
8
9
|
export const CPP_DEV_IMAGE = "muscat-pk-cpp-dev";
|
|
9
10
|
|
|
@@ -14,9 +15,16 @@ export function assertDockerReady() {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
|
|
17
|
-
export async function ensureImage(image = CPP_DEV_IMAGE, { platform = null } = {}) {
|
|
18
|
+
export async function ensureImage(image = CPP_DEV_IMAGE, { platform = null, root = pluginRootOrNull() } = {}) {
|
|
19
|
+
await buildImage(image, platform, root);
|
|
20
|
+
// Dev images feed the editor's SDK headers: the native one (dev) or the host-arch one (build).
|
|
21
|
+
const hostArch = process.arch === "arm64" ? "arm64" : "amd64";
|
|
22
|
+
if (root && (image === CPP_DEV_IMAGE || image === `${CPP_DEV_IMAGE}-${hostArch}`)) syncCppIde(root, image);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function buildImage(image, platform, root) {
|
|
18
26
|
const dockerfile = await resolveDockerfile("Dockerfile.cpp");
|
|
19
|
-
const sdkVer =
|
|
27
|
+
const sdkVer = cppSdkVersion(root);
|
|
20
28
|
const key = imageKey(dockerfile, sdkVer ?? "");
|
|
21
29
|
if (canSkipImageBuild(image, key)) {
|
|
22
30
|
log.step(`${image} already built — skipping docker build`);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Editor (IntelliSense) support for C++ backends. The rb_sdk headers only exist inside the dev image,
|
|
2
|
+
// so a host editor flags every SDK name. Copy them to .muscat-dev/sysroot and point
|
|
3
|
+
// .vscode/c_cpp_properties.json at them, using the include dirs / defines rb_sdk's CMake target exports.
|
|
4
|
+
// The build never reads these copies — it runs in the image.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { cppSdkFiles, log } from "./util.mjs";
|
|
9
|
+
|
|
10
|
+
const SYSROOT = ".muscat-dev/sysroot";
|
|
11
|
+
const CONFIG_NAME = "Muscat";
|
|
12
|
+
const COPY = ["/usr/include/rainbow", "/usr/include/eigen3", "/usr/lib/cmake/rb_sdk/rb_sdkTargets.cmake"];
|
|
13
|
+
|
|
14
|
+
function imageId(image) {
|
|
15
|
+
const r = spawnSync("docker", ["image", "inspect", "-f", "{{.Id}}", image], { encoding: "utf8" });
|
|
16
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Copy the SDK headers out of `image` (once per image build) and write the VS Code C/C++ config. */
|
|
20
|
+
export function syncCppIde(root, image) {
|
|
21
|
+
if (!cppSdkFiles(root).length) return;
|
|
22
|
+
const id = imageId(image);
|
|
23
|
+
if (!id) return;
|
|
24
|
+
const sysroot = resolve(root, SYSROOT);
|
|
25
|
+
const stamp = resolve(sysroot, ".image-id");
|
|
26
|
+
if (!existsSync(stamp) || readFileSync(stamp, "utf8") !== id) {
|
|
27
|
+
rmSync(sysroot, { recursive: true, force: true });
|
|
28
|
+
const create = spawnSync("docker", ["create", image], { encoding: "utf8" });
|
|
29
|
+
if (create.status !== 0) return log.warn("could not copy SDK headers for the editor (docker create failed).");
|
|
30
|
+
const container = create.stdout.trim();
|
|
31
|
+
try {
|
|
32
|
+
for (const src of COPY) {
|
|
33
|
+
const dest = resolve(sysroot, src.slice(1));
|
|
34
|
+
mkdirSync(resolve(dest, ".."), { recursive: true });
|
|
35
|
+
spawnSync("docker", ["cp", `${container}:${src}`, dest], { stdio: "ignore" });
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
spawnSync("docker", ["rm", container], { stdio: "ignore" });
|
|
39
|
+
}
|
|
40
|
+
writeFileSync(stamp, id);
|
|
41
|
+
log.step(`SDK headers for the editor → ${SYSROOT}`);
|
|
42
|
+
}
|
|
43
|
+
writeCppProperties(root, readTarget(sysroot));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// INTERFACE_INCLUDE_DIRECTORIES / INTERFACE_COMPILE_DEFINITIONS of rb_sdk::rb_sdk, mapped into the sysroot.
|
|
47
|
+
function readTarget(sysroot) {
|
|
48
|
+
const file = resolve(sysroot, "usr/lib/cmake/rb_sdk/rb_sdkTargets.cmake");
|
|
49
|
+
const text = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
50
|
+
const list = (key) => (text.match(new RegExp(`${key} "([^"]*)"`))?.[1] ?? "").split(";").filter(Boolean);
|
|
51
|
+
const prefix = `\${workspaceFolder}/${SYSROOT}/usr`;
|
|
52
|
+
const includes = list("INTERFACE_INCLUDE_DIRECTORIES").map((d) => d.replace("${_IMPORT_PREFIX}", prefix));
|
|
53
|
+
return {
|
|
54
|
+
includePath: [...(includes.length ? includes : [`${prefix}/include`]), `${prefix}/include/eigen3`],
|
|
55
|
+
defines: list("INTERFACE_COMPILE_DEFINITIONS"),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Keeps other configurations; ours is replaced in place or put first so VS Code picks it by default.
|
|
60
|
+
function writeCppProperties(root, { includePath, defines }) {
|
|
61
|
+
const file = resolve(root, ".vscode", "c_cpp_properties.json");
|
|
62
|
+
let props = { configurations: [], version: 4 };
|
|
63
|
+
if (existsSync(file)) {
|
|
64
|
+
try {
|
|
65
|
+
props = JSON.parse(readFileSync(file, "utf8"));
|
|
66
|
+
} catch {
|
|
67
|
+
return log.warn(".vscode/c_cpp_properties.json is not plain JSON — add the SDK include paths yourself.");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const config = { name: CONFIG_NAME, includePath: ["${workspaceFolder}/**", ...includePath], defines, cppStandard: "c++20" };
|
|
71
|
+
const configs = (props.configurations ?? []).filter((c) => c.name !== CONFIG_NAME);
|
|
72
|
+
props.configurations = [config, ...configs];
|
|
73
|
+
mkdirSync(resolve(root, ".vscode"), { recursive: true });
|
|
74
|
+
writeFileSync(file, JSON.stringify(props, null, 2) + "\n");
|
|
75
|
+
}
|
package/cli/src/dev.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// muscat-pk dev — run the
|
|
1
|
+
// muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common), dev:ui, and dev:back together.
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { connect as netConnect } from "node:net";
|
|
@@ -6,7 +6,7 @@ import { resolve } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { runDevUi } from "./devUi.mjs";
|
|
8
8
|
import { runDevBack, stopDevBackContainers } from "./devBack.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { ensureLocalMuscat, GATEWAY_PORT, isPortInUse } from "./muscatLocal.mjs";
|
|
10
10
|
import { assertManifestPorts, assertManifestVariables } from "./manifest.mjs";
|
|
11
11
|
import { formatPortRanges } from "./ports.mjs";
|
|
12
12
|
import {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
promptRobotConnection,
|
|
19
19
|
venvPython,
|
|
20
20
|
} from "./util.mjs";
|
|
21
|
-
import { ensureRemoteZenohEnv, isRemoteRobot, isWsl
|
|
21
|
+
import { ensureRemoteZenohEnv, isRemoteRobot, isWsl } from "./zenohDev.mjs";
|
|
22
22
|
|
|
23
23
|
const HOST_AGENT_PATH = fileURLToPath(new URL("./host_agent.py", import.meta.url));
|
|
24
24
|
const canAutoStartHostAgent = () => process.platform === "darwin" || isWsl();
|
|
@@ -99,7 +99,7 @@ async function maybeStartHostAgent(root, { robotIp, robotToken }) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// advertiseHost (not host.docker.internal): a Docker Desktop container run
|
|
102
|
-
// under `dev`
|
|
102
|
+
// under `dev` shares the local Muscat container's netns (--network container:),
|
|
103
103
|
// which has no host-gateway alias configured — the host's real LAN IP
|
|
104
104
|
// always resolves regardless of netns mode.
|
|
105
105
|
const proxyEndpoint = `${advertiseHost}:${proxyPort}`;
|
|
@@ -126,32 +126,26 @@ export async function runDev(args = []) {
|
|
|
126
126
|
const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
|
|
127
127
|
const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT || "";
|
|
128
128
|
// No proxy (Linux, disabled, ...): fall back to the container reverse-dialing
|
|
129
|
-
// the robot itself —
|
|
129
|
+
// the robot itself — the local Muscat then publishes the zenoh port for it.
|
|
130
130
|
if (!proxyEndpoint) await ensureRemoteZenohEnv(robotIp, { log });
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const muscat = bridgeStarted ? null : localMuscatContainer();
|
|
132
|
+
// Robot or not, the local Muscat runtime is the gateway (:10000), MediaMTX and common; the
|
|
133
|
+
// backend joins its netns, so the plugin's declared ports are published from it too.
|
|
134
|
+
log.step("starting local Muscat (Docker) + dev:ui + dev:back");
|
|
135
|
+
const muscat = await ensureLocalMuscat({
|
|
136
|
+
pluginPorts: portRanges,
|
|
137
|
+
zenohPort: remoteRobot && !proxyEndpoint ? Number(process.env.MUSCAT_ZENOH_PORT) || null : null,
|
|
138
|
+
}).catch((e) => { log.warn(`local Muscat: ${e.message}`); return null; });
|
|
142
139
|
if (muscat) {
|
|
143
|
-
log.step(`local Muscat container '${muscat}' owns :${
|
|
140
|
+
log.step(`local Muscat container '${muscat}' owns :${GATEWAY_PORT} — backend joins its network (gateway, MediaMTX)`);
|
|
144
141
|
process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
|
|
145
|
-
} else if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (answer.toLowerCase() !== "y") return;
|
|
149
|
-
log.info("proceeding without bridge — backend will skip network sharing.");
|
|
142
|
+
} else if (await isPortInUse(GATEWAY_PORT)) {
|
|
143
|
+
// e.g. developing on the robot PC: its Muscat already serves the gateway; the backend reaches it via the host.
|
|
144
|
+
log.step(`:${GATEWAY_PORT} is already served (Muscat on this machine) — using it`);
|
|
150
145
|
} else {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (bridgeStarted.media) process.env.MUSCAT_PK_MEDIA = "1";
|
|
146
|
+
log.warn(`no local Muscat on :${GATEWAY_PORT} — the plugin UI cannot reach the backend.`);
|
|
147
|
+
const answer = await prompt(" Proceed anyway? [y/N]: ");
|
|
148
|
+
if (answer.toLowerCase() !== "y") return;
|
|
155
149
|
}
|
|
156
150
|
|
|
157
151
|
process.env.MUSCAT_PK_MANAGED_DEV = "1";
|
|
@@ -159,7 +153,6 @@ export async function runDev(args = []) {
|
|
|
159
153
|
const cleanup = () => {
|
|
160
154
|
if (hostAgent && hostAgent.exitCode === null) hostAgent.kill();
|
|
161
155
|
stopDevBackContainers();
|
|
162
|
-
if (bridgeStarted) stopBridge();
|
|
163
156
|
};
|
|
164
157
|
process.once("SIGINT", cleanup);
|
|
165
158
|
process.once("SIGTERM", cleanup);
|
package/cli/src/devBack.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Every backend runs in a warm Docker container that matches the robot
|
|
3
3
|
// (Ubuntu 24.04), so dev behaves like production.
|
|
4
4
|
// python : Python 3.12 container, pip install, run main.py with watchfiles
|
|
5
|
-
// hot-reload. Shares the
|
|
5
|
+
// hot-reload. Shares the local Muscat container's netns (localhost:10000).
|
|
6
6
|
// cpp : Ubuntu + cmake container, build cpp/, run the binary, rebuild +
|
|
7
7
|
// restart on source changes.
|
|
8
8
|
// ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
|
|
@@ -12,7 +12,8 @@ import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:
|
|
|
12
12
|
import { isIP } from "node:net";
|
|
13
13
|
import { dirname, resolve } from "node:path";
|
|
14
14
|
import { dockerUserFlags, ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
|
|
15
|
-
import { ensureRemoteZenohEnv, usesHostNetwork } from "./zenohDev.mjs";
|
|
15
|
+
import { ensureRemoteZenohEnv, isRemoteRobot, usesHostNetwork } from "./zenohDev.mjs";
|
|
16
|
+
import { localMuscatContainer } from "./muscatLocal.mjs";
|
|
16
17
|
import { PLUGIN_ID_RE, assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, rosNamespace, validateManifest } from "./manifest.mjs";
|
|
17
18
|
import {
|
|
18
19
|
formatPortRanges,
|
|
@@ -53,15 +54,22 @@ export function stopDevBackContainers() {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
// Where the backend's 127.0.0.1 points. A local Muscat container (muscat-dev):
|
|
57
|
-
// netns so the gateway and MediaMTX resolve like on the robot.
|
|
58
|
-
// netns
|
|
57
|
+
// Where the backend's 127.0.0.1 points. A local Muscat container (muscat-pk-muscat, muscat-dev):
|
|
58
|
+
// join its netns so the gateway and MediaMTX resolve like on the robot. Otherwise its own
|
|
59
|
+
// netns, gateway via the host.
|
|
60
|
+
// IS_DEV Muscat (muscat-dev): common is a zenoh-router client and opens no 7447 listener,
|
|
61
|
+
// so the plugin must join the router too or `execute` queries never reach it.
|
|
62
|
+
function muscatZenohEnv(container) {
|
|
63
|
+
const r = spawnSync("docker", ["inspect", "-f", "{{range .Config.Env}}{{println .}}{{end}}", container], { encoding: "utf8" });
|
|
64
|
+
return /^IS_DEV=(true|1)$/im.test(r.stdout || "")
|
|
65
|
+
? ["-e", "ZENOH_PEER_CONNECT_ENDPOINTS=tcp/zenoh-router:7447"]
|
|
66
|
+
: [];
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
function backendNetwork(portRanges) {
|
|
60
70
|
const muscat = process.env.MUSCAT_PK_MUSCAT_CONTAINER;
|
|
61
|
-
if (muscat) return { run: ["--network", `container:${muscat}`], env:
|
|
71
|
+
if (muscat) return { run: ["--network", `container:${muscat}`], env: muscatZenohEnv(muscat), shared: true };
|
|
62
72
|
if (usesHostNetwork()) return { run: ["--network", "host"], env: [], shared: true };
|
|
63
|
-
const bridge = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
|
|
64
|
-
if (bridge) return { run: ["--network", `container:${bridge}`], env: [], shared: true };
|
|
65
73
|
return {
|
|
66
74
|
// host.docker.internal is automatic on Docker Desktop; this makes it resolve on Linux too.
|
|
67
75
|
run: [...publishPortFlags(portRanges), "--add-host", "host.docker.internal:host-gateway"],
|
|
@@ -107,13 +115,6 @@ async function serveRosPeers(root, pluginId, restart) {
|
|
|
107
115
|
});
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
// Our own MediaMTX, only when `dev` found its ports free (a local Muscat brings its own).
|
|
111
|
-
function startMediaServer(container) {
|
|
112
|
-
if (process.env.MUSCAT_PK_MEDIA !== "1") return;
|
|
113
|
-
spawnSync("docker", ["exec", "-d", container, "mediamtx", "/etc/mediamtx/mediamtx.yml"], { stdio: "ignore" });
|
|
114
|
-
log.step("MediaMTX: publish rtsp://127.0.0.1:8554/<path> · play http://localhost:8889/<path>");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
118
|
// ── IDE tooling venv (best-effort, non-blocking) ────────────────────────────
|
|
118
119
|
// `muscat-pk new` already creates this up front (see new.mjs); this just
|
|
119
120
|
// refreshes it on every `dev`/`dev:back` in case requirements.txt changed.
|
|
@@ -134,8 +135,8 @@ function syncIdeVenv(root) {
|
|
|
134
135
|
|
|
135
136
|
// ── python ────────────────────────────────────────────────────────────────────
|
|
136
137
|
// Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
|
|
137
|
-
// Under `muscat-pk dev`, it shares the
|
|
138
|
-
// reaches the
|
|
138
|
+
// Under `muscat-pk dev`, it shares the local Muscat container's netns, so the SDK
|
|
139
|
+
// reaches the plugin gateway at the default localhost:10000.
|
|
139
140
|
async function runPython(root, portRanges) {
|
|
140
141
|
// Independent of Docker readiness — must still happen if Docker isn't
|
|
141
142
|
// installed/running, since that's exactly when local IDE tooling matters most.
|
|
@@ -154,7 +155,7 @@ async function runPython(root, portRanges) {
|
|
|
154
155
|
// Zenoh mesh, so it speaks a byte protocol to the host-side proxy instead.
|
|
155
156
|
const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT ?? "";
|
|
156
157
|
// Native Linux (not WSL, not Docker Desktop) has real host networking: the
|
|
157
|
-
// container shares the host's network stack, so it reaches the
|
|
158
|
+
// container shares the host's network stack, so it reaches the gateway at
|
|
158
159
|
// localhost:10000 and the robot's Zenoh router / reverse-dial path directly
|
|
159
160
|
// — no netns sharing, port publishing, host.docker.internal, or byte-proxy.
|
|
160
161
|
const net = backendNetwork(portRanges);
|
|
@@ -195,7 +196,6 @@ async function runPython(root, portRanges) {
|
|
|
195
196
|
{ stdio: "inherit" },
|
|
196
197
|
);
|
|
197
198
|
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
198
|
-
startMediaServer(container);
|
|
199
199
|
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [zenohPort] });
|
|
200
200
|
|
|
201
201
|
log.step("pip install requirements + watchfiles (in container)…");
|
|
@@ -413,7 +413,7 @@ runpy.run_path("/plugin/py/main.py", run_name="__main__")
|
|
|
413
413
|
PY`]);
|
|
414
414
|
|
|
415
415
|
if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
|
|
416
|
-
if (usesHostNetwork()) log.step("network: host (native Linux — direct robot/
|
|
416
|
+
if (usesHostNetwork()) log.step("network: host (native Linux — direct robot/gateway access)");
|
|
417
417
|
if (proxyEndpoint) log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
|
|
418
418
|
else if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
|
|
419
419
|
log.step("watchfiles → py/main.py (reload on any .py change in py/)");
|
|
@@ -493,7 +493,6 @@ async function runRos2(root, portRanges) {
|
|
|
493
493
|
{ stdio: "inherit" },
|
|
494
494
|
);
|
|
495
495
|
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
496
|
-
startMediaServer(container);
|
|
497
496
|
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
|
|
498
497
|
|
|
499
498
|
// one-time deps + symlink build (edits to .py reflect without rebuilding)
|
|
@@ -586,7 +585,6 @@ async function runCpp(root, portRanges) {
|
|
|
586
585
|
{ stdio: "inherit" },
|
|
587
586
|
);
|
|
588
587
|
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
589
|
-
startMediaServer(container);
|
|
590
588
|
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
|
|
591
589
|
|
|
592
590
|
const build = () => {
|
|
@@ -656,6 +654,15 @@ export async function runDevBack(args = []) {
|
|
|
656
654
|
// Byte-proxy mode (a host-side host_agent set MUSCAT_PROXY_ENDPOINT) makes
|
|
657
655
|
// the container speak the proxy protocol instead, so it needs no reverse-dial.
|
|
658
656
|
if (!process.env.MUSCAT_PROXY_ENDPOINT) await ensureRemoteZenohEnv(robotIp, { log });
|
|
657
|
+
// Standalone against a local Muscat (muscat-dev, …): join its netns like `dev` does,
|
|
658
|
+
// otherwise zenoh reaches nothing and `execute` never arrives.
|
|
659
|
+
if (!process.env.MUSCAT_PK_MUSCAT_CONTAINER && !isRemoteRobot(robotIp)) {
|
|
660
|
+
const muscat = localMuscatContainer();
|
|
661
|
+
if (muscat) {
|
|
662
|
+
log.step(`local Muscat container '${muscat}' found — backend joins its network`);
|
|
663
|
+
process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
659
666
|
const type = manifestRuntime(root);
|
|
660
667
|
if (type === "ros2") return await runRos2(root, portRanges);
|
|
661
668
|
if (type === "cpp") return await runCpp(root, portRanges);
|
package/cli/src/devUi.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { checkKitUpdate, findPluginRoot, hasBin, kitRoot, log, parseArgs, prompt } from "./util.mjs";
|
|
10
|
+
import { ensureLocalMuscat } from "./muscatLocal.mjs";
|
|
11
|
+
import { isRemoteRobot } from "./zenohDev.mjs";
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
const DEV_PLUGIN_PORT = 4181;
|
|
@@ -44,7 +46,9 @@ function findPreviewHostRoot() {
|
|
|
44
46
|
try {
|
|
45
47
|
const req = createRequire(import.meta.url);
|
|
46
48
|
return dirname(req.resolve(`${PREVIEW_PKG}/package.json`));
|
|
47
|
-
} catch {
|
|
49
|
+
} catch {
|
|
50
|
+
console.log("Try the sibling and cached package locations below.")
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
const sibling = resolve(kitRoot, "../preview-host");
|
|
50
54
|
if (existsSync(resolve(sibling, "package.json"))) return sibling;
|
|
@@ -137,6 +141,7 @@ function waitForPluginDevPort(readyFile, proc, timeoutMs = 15_000) {
|
|
|
137
141
|
const { port } = JSON.parse(readFileSync(readyFile, "utf8"));
|
|
138
142
|
if (Number.isInteger(port)) return res(port);
|
|
139
143
|
} catch {
|
|
144
|
+
console.log("Keep polling until the ready file contains valid JSON.")
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
if (Date.now() - start > timeoutMs)
|
|
@@ -160,6 +165,10 @@ export async function runDevUi(args = []) {
|
|
|
160
165
|
log.warn(`preview-host port ${requestedPort} is in use — using ${port} instead.`);
|
|
161
166
|
|
|
162
167
|
if (robotIp) log.info(`MUSCAT_API_ORIGIN → http://${robotIp}:*`);
|
|
168
|
+
// No robot: the bundle talks to localhost:3000, so put a Muscat there (`dev` already did).
|
|
169
|
+
if (!isRemoteRobot(robotIp) && process.env.MUSCAT_PK_MANAGED_DEV !== "1") {
|
|
170
|
+
await ensureLocalMuscat().catch((e) => log.warn(`local Muscat: ${e.message}`));
|
|
171
|
+
}
|
|
163
172
|
|
|
164
173
|
watchAssets(pluginRoot, outDir);
|
|
165
174
|
|