@rainbow-robotics/muscat-plugin-kit 1.10.3-dev.2 → 1.10.3-dev.4
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.ko.md +1185 -0
- package/README.md +1051 -148
- package/cli/README.md +4 -2
- package/cli/bin/muscat-pk.mjs +6 -2
- package/cli/src/build.mjs +9 -13
- package/cli/src/completion.mjs +2 -0
- package/cli/src/dev.mjs +22 -3
- package/cli/src/devBack.mjs +1 -1
- package/cli/src/devUi.mjs +2 -1
- package/cli/src/mcp.mjs +17 -3
- package/cli/src/muscatLocal.mjs +57 -21
- package/cli/src/pluginDevServer.mjs +1 -1
- package/cli/src/sim.mjs +163 -0
- package/cli/src/util.mjs +2 -0
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-JZmvrBu2.js → Color-YHDXOIA2-DSB_yvex.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DjjxchlZ.js → Docs-C_90bDRy.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DrMHaIyt.js → Docs-CbW9HZd4.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-GWKBJz_w.js → Docs-CoskoRMT.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CHh5amiz.js → Docs-Cy9XFeLJ.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DdU-wsGN.js → Docs-De1oc64k.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DXJ25Kqq.js → Docs-klD3F_Ai.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-DNcXzd9o.js → DocsRenderer-CFRXHY34-Bkm4uV_D.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-CwnK8AUF.js → NxAvatar.stories-Ctf5G2FQ.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-ueYdosfQ.js → NxButton.stories-DeLObVSj.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-M5ZCLbrK.js → NxNumberInput.stories-LqoY0ed1.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-bxxUnHEL.js → NxPagination.stories-DI6bXuJM.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BHxB2kr5.js → NxPointerHoldButton.stories-3HSV1XmN.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-BKfr-fp4.js → NxRangeInput.stories-1pjgQVaG.js} +1 -1
- package/cli/storybook-dist/assets/{NxSelectBox.stories-CG8FuTBq.js → NxSelectBox.stories-bcQPZEuF.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-BRTFtMTL.js → NxSlideRange.stories-BosX4axk.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-Bwuyzn-R.js → NxSwitch.stories-DxV6dS33.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-COq2sGqu.js → NxTextArea.stories-CO545-LP.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-CjsBLz05.js → NxTextInput.stories-BP0npai1.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-BARwuZ0Q.js → RBAccordion.stories-fVR4Va2a.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-COrPxETW.js → RBAccordionGroup.stories-Du5bsckD.js} +1 -1
- package/cli/storybook-dist/assets/{RBAlert.stories-BQrkjZPt.js → RBAlert.stories-DNkMXUu6.js} +1 -1
- package/cli/storybook-dist/assets/{RBBadge.stories-BTsLL-lK.js → RBBadge.stories-C44pXt8D.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-DAeQdJbc.js → RBBattery.stories-DE2JHvfL.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-HyhOE80R.js → RBCard.stories-CuIda7NI.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckBox.stories-KFk0WqX_.js → RBCheckBox.stories-D1qM1ayr.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CXbbktF5.js → RBCheckIconButton.stories-e4gkliBO.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-DWfYz_9Z.js → RBCheckLabelButton.stories-CDzCtsMI.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipTab.stories-JPEgVFAS.js → RBChipTab.stories-DBrc_3Mp.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-CTPuWXrQ.js → RBChipsBoxTab.stories-hlrsHjKY.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-DiFRZ5jk.js → RBColorIcons.stories-BHUvpkdu.js} +1 -1
- package/cli/storybook-dist/assets/{RBCommand.stories-Pf1HAVIR.js → RBCommand.stories-azAF41NP.js} +1 -1
- package/cli/storybook-dist/assets/{RBConfirm.stories-D1cEPUXk.js → RBConfirm.stories-BMNjwbmT.js} +1 -1
- package/cli/storybook-dist/assets/{RBDateInput.stories-RPYpCjig.js → RBDateInput.stories-D0VezDzv.js} +1 -1
- package/cli/storybook-dist/assets/{RBDialog.stories-DJH-0XSR.js → RBDialog.stories-AOJ975Fy.js} +1 -1
- package/cli/storybook-dist/assets/{RBDivider.stories-uEj1vOgK.js → RBDivider.stories-NSvnj43Z.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-BiMzElb2.js → RBDrawer.stories-Du0oTvD1.js} +1 -1
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-Da_TBkla.js → RBDropdownMenu.stories-Hsxve4PW.js} +1 -1
- package/cli/storybook-dist/assets/{RBEmptyView.stories-BHeKGfPS.js → RBEmptyView.stories-BlwHx3zp.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-NEsRFoYp.js → RBIconButton.stories-DAPDY63x.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-CcZ-TTLS.js → RBIcons.stories-W5ZUV2L1.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-B0wpdQyR.js → RBInlineLabelButton.stories-C99j0Y3g.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-DEgBou3K.js → RBJoyStick.stories-Du0H1iAS.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-DT8drIsF.js → RBLabelButton.stories-BlvHaU_Z.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-Dh-aoeyG.js → RBLight.stories-BY6DKrre.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-D-H7UqVh.js → RBMenubar.stories-D2PEGpM9.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-hlclv6jI.js → RBMultiRangeInput.stories-Bm2qSmjq.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-DWCeoZ_S.js → RBPagination.stories-DDleJa5N.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-C514M09_.js → RBPopover.stories-27YxPSFG.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-BSItvfiT.js → RBPrompt.stories-BFaIOctr.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-BBb4YbFU.js → RBRadioBox.stories-BroDar0t.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-CXyOGJCv.js → RBRadioButton.stories-M3O-wX7V.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-qvhubu0e.js → RBRangeDateInput.stories-CWlM_D5X.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-CZKeyGjH.js → RBResizable.stories-ui8X1qnc.js} +1 -1
- package/cli/storybook-dist/assets/{RBSelectBox.stories-CRKP3ybf.js → RBSelectBox.stories-C8T6cfA0.js} +1 -1
- package/cli/storybook-dist/assets/{RBSheet.stories-sCM3ZJUa.js → RBSheet.stories-BCOo_Zcc.js} +1 -1
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-Bb7gw8RF.js → RBSideNumberInput.stories-CU2_MVlJ.js} +1 -1
- package/cli/storybook-dist/assets/{RBSlider.stories-23OAQU5z.js → RBSlider.stories-DgOp1TFc.js} +1 -1
- package/cli/storybook-dist/assets/{RBSpinner.stories-2xcQdcmt.js → RBSpinner.stories-qLScY3zZ.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-BACUaJm_.js → RBSwitch.stories-SXSY1QMb.js} +1 -1
- package/cli/storybook-dist/assets/{RBTab.stories-CDHE3sVs.js → RBTab.stories-BzXXTsXP.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-C0Cfhfl7.js → RBTable.stories-DHTeRNVk.js} +1 -1
- package/cli/storybook-dist/assets/{RBTableRow.stories-BF8kIkpp.js → RBTableRow.stories-DRNRsd15.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-BdJWRq1o.js → RBTabs.stories-DP0noBLp.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextArea.stories-CGy2xeYH.js → RBTextArea.stories-TkaejfxJ.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextField.stories-IfUOzxCM.js → RBTextField.stories-B6VpCGyt.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextInput.stories-VjXNFo4R.js → RBTextInput.stories-D3hZ08e-.js} +1 -1
- package/cli/storybook-dist/assets/{RBToast.stories-BzcobblV.js → RBToast.stories-BQ2wS2U0.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-DuLE5mKv.js → RBToggle.stories-GVOjGqo3.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-CuR0N0GJ.js → RBToggleGroup.stories-pzx0btOD.js} +1 -1
- package/cli/storybook-dist/assets/{RBTooltip.stories--aAgNC8_.js → RBTooltip.stories-jufB6fa1.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-DyWQxYUj.js → SideNumberInput-BYlmNHEw.js} +1 -1
- package/cli/storybook-dist/assets/{entry-preview-Du01HPfn.js → entry-preview-DY0ZrYNE.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-DFBATQRS.js → iframe-LwKb1w63.js} +2 -2
- package/cli/storybook-dist/assets/{index-DG8XddNr.js → index-BsAhjDKO.js} +3 -3
- package/cli/storybook-dist/assets/preview-BJ0FvQZh.js +3 -0
- package/cli/storybook-dist/assets/{preview-donYBfDJ.js → preview-DiY2_30w.js} +2 -2
- package/cli/storybook-dist/assets/{preview-CigNHo-I.css → preview-kTZPgP6D.css} +1 -1
- package/cli/storybook-dist/assets/{react-resizable-panels-Dp9zi-o3.css → react-resizable-panels-HsFbxPbn.css} +1 -1
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/README.md +2 -3
- package/cli/templates/common/requirements.txt +3 -3
- package/cli/templates/cpp/cpp/CMakeLists.txt +1 -1
- package/cli/templates/readme-cpp/README.md +3 -4
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +1 -1
- package/package.json +5 -4
- package/cli/storybook-dist/assets/preview-e0NAgyy_.js +0 -3
- /package/cli/storybook-dist/assets/{react-resizable-panels.browser-8hC8dr-6.js → react-resizable-panels.browser-DuxpZLkF.js} +0 -0
package/cli/README.md
CHANGED
|
@@ -11,7 +11,7 @@ muscat-pk new my-plugin
|
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
muscat-pk new my-plugin [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun] [--yes]
|
|
14
|
-
muscat-pk build # ui/content.tsx -> dist/ (minified) + distributable ZIP
|
|
14
|
+
muscat-pk build # ui/content.tsx -> dist/ (minified) + distributable ZIP
|
|
15
15
|
muscat-pk dev:ui # preview the plugin in a mini-Muscat (preview-host)
|
|
16
16
|
muscat-pk dev:back # run the back end in a Docker container (python / cpp / ros2)
|
|
17
17
|
muscat-pk dev # dev:ui + dev:back
|
|
@@ -229,8 +229,10 @@ inside plugins.
|
|
|
229
229
|
at `/plugin-dev` — a real mini-Muscat shell. Without a robot IP it needs a Muscat
|
|
230
230
|
on `localhost:3000` for program preview: `dev` / `dev:ui` reuse any container that
|
|
231
231
|
already publishes `:10000` (`muscat`, `muscat-dev`, …) and otherwise start
|
|
232
|
-
`muscat-pk-muscat` — the S3 runtime image (`muscat/main/muscat.zip`) running
|
|
232
|
+
`muscat-pk-muscat` — the S3 runtime image (`muscat/main/muscat.zip`) running the
|
|
233
233
|
`common` service from `muscat/main/version.json`, cached under `~/.muscat-pk/muscat/`.
|
|
234
|
+
`dev --sim <model>` adds the services the model needs (log + manipulate / rby1), opens
|
|
235
|
+
common's Zenoh on the `muscat-pk` network, and starts simulators there (`cli/src/sim.mjs`).
|
|
234
236
|
It keeps running between runs (`docker rm -f muscat-pk-muscat` to stop);
|
|
235
237
|
`MUSCAT_PK_NO_LOCAL_MUSCAT=1` disables it.
|
|
236
238
|
|
package/cli/bin/muscat-pk.mjs
CHANGED
|
@@ -87,8 +87,8 @@ function formatHelp() {
|
|
|
87
87
|
color.bold("Commands:"),
|
|
88
88
|
commandRow("new [dir]", `Scaffold a new plugin project ${color.dim("(prompts: author, email, description, type, front-package-manager)")}`),
|
|
89
89
|
commandRow("type change", `Switch backend type ${color.dim("(python | cpp | ros2-python | ros2-cpp)")}`),
|
|
90
|
-
commandRow("build", `Build + lock deps + vendor wheels + zip ${color.dim(`(${option("--
|
|
91
|
-
commandRow("dev",
|
|
90
|
+
commandRow("build", `Build + lock deps + vendor wheels + zip ${color.dim(`(${option("--arch")} amd64|arm64|both)`)}`),
|
|
91
|
+
commandRow("dev", `Run a local Muscat runtime, dev:ui, and dev:back together ${color.dim(`(${option("--sim")} 500880|500920|RBY2|rby1-a|rby1-m|rby1-ub)`)}`),
|
|
92
92
|
commandRow("dev:ui", `Preview the plugin in a mini-Muscat ${color.dim("(preview-host)")}`),
|
|
93
93
|
commandRow("dev:back", `Run the plugin back end ${color.dim("(python: watchfiles reload / ros2: Docker)")}`),
|
|
94
94
|
commandRow("storybook", `Browse the bundled rb-components Storybook ${color.dim(`(${option("--port")} <n>, ${option("--rebuild")})`)}`),
|
|
@@ -96,6 +96,7 @@ function formatHelp() {
|
|
|
96
96
|
commandRow("update", `Update ${color.magenta("@rainbow-robotics/muscat-plugin-kit")}, ${color.magenta("@rainbow-robotics/plugin-ui")} and ${color.magenta("@rainbow-robotics/preview-host")} ${color.dim(`(local-only if run from a project's local install; ${option("--global")} also updates the global CLI)`)}`),
|
|
97
97
|
commandRow("uninstall", `Remove the installed ${color.cyan("muscat-pk")} CLI ${color.dim("(npm global or S3 binary)")}`),
|
|
98
98
|
commandRow("sdk <list|update [version]>", "Manage the python rainbow-rb-sdk version"),
|
|
99
|
+
commandRow("sim list", `List the robot simulators for ${option("dev --sim")}`),
|
|
99
100
|
commandRow("version", `Print the installed ${color.cyan("muscat-pk")} version`),
|
|
100
101
|
commandRow("completion [shell]", `Print a shell completion script ${color.dim("(bash | zsh | fish | powershell)")}`),
|
|
101
102
|
"",
|
|
@@ -137,6 +138,9 @@ async function main() {
|
|
|
137
138
|
return await runUninstall(args);
|
|
138
139
|
case "sdk":
|
|
139
140
|
return await runSdk(args);
|
|
141
|
+
case "sim":
|
|
142
|
+
if (args[0] !== "list") throw new Error("usage: muscat-pk sim list");
|
|
143
|
+
return (await import("../src/sim.mjs")).runSimList();
|
|
140
144
|
case "completion":
|
|
141
145
|
return await runCompletion(args);
|
|
142
146
|
case "version":
|
package/cli/src/build.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// muscat-pk build — bundle ui/content.tsx into dist/content.js, stage
|
|
2
2
|
// manifest.json and assets/ for distribution, and zip dist/ into a
|
|
3
|
-
// distributable ZIP
|
|
3
|
+
// distributable ZIP.
|
|
4
4
|
//
|
|
5
5
|
// sandbox.html is generated by the CLI — plugin authors never manage it.
|
|
6
6
|
// The import map always reflects the current plugin-ui entry set, so users
|
|
@@ -407,8 +407,6 @@ async function promptVersion(root) {
|
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
export async function runBuild(args) {
|
|
410
|
-
const noZip = args.includes("--no-zip");
|
|
411
|
-
const noLock = args.includes("--no-lock");
|
|
412
410
|
const root = findPluginRoot();
|
|
413
411
|
const rt = manifestRuntime(root);
|
|
414
412
|
|
|
@@ -424,19 +422,17 @@ export async function runBuild(args) {
|
|
|
424
422
|
const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
|
|
425
423
|
|
|
426
424
|
// python / rclpy: lock + wheel vendor in Ubuntu 24.04 container
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
await lockAndVendor(pluginRoot, archs);
|
|
435
|
-
}
|
|
425
|
+
const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime ?? {};
|
|
426
|
+
const needsLock =
|
|
427
|
+
manifest.type === "python" ||
|
|
428
|
+
(manifest.type === "ros2" && manifest.rosClient === "rclpy");
|
|
429
|
+
if (needsLock) {
|
|
430
|
+
const { lockAndVendor } = await import("./python.mjs");
|
|
431
|
+
await lockAndVendor(pluginRoot, archs);
|
|
436
432
|
}
|
|
437
433
|
|
|
438
434
|
await exportRos2Build(root, archs);
|
|
439
435
|
await exportCppBuild(root, distDir, archs);
|
|
440
436
|
calcInstalledSize(pluginRoot);
|
|
441
|
-
|
|
437
|
+
packZip(pluginRoot);
|
|
442
438
|
}
|
package/cli/src/completion.mjs
CHANGED
|
@@ -20,6 +20,7 @@ const COMMANDS = [
|
|
|
20
20
|
["update", "Update muscat-pk and plugin-ui"],
|
|
21
21
|
["uninstall", "Remove the installed muscat-pk CLI"],
|
|
22
22
|
["sdk", "Manage the rainbow-rb-sdk version (python: requirements.txt, C++: CMakeLists.txt)"],
|
|
23
|
+
["sim", "List the robot simulators for dev --sim"],
|
|
23
24
|
["version", "Print the installed muscat-pk version"],
|
|
24
25
|
["completion", "Print a shell completion script"],
|
|
25
26
|
["help", "Show help"],
|
|
@@ -28,6 +29,7 @@ const COMMANDS = [
|
|
|
28
29
|
const SUBCOMMANDS = {
|
|
29
30
|
type: ["change"],
|
|
30
31
|
sdk: ["list", "update"],
|
|
32
|
+
sim: ["list"],
|
|
31
33
|
completion: ["bash", "zsh", "fish", "powershell"],
|
|
32
34
|
};
|
|
33
35
|
|
package/cli/src/dev.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
// muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common),
|
|
2
|
-
|
|
1
|
+
// muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common), robot simulators (--sim),
|
|
2
|
+
// dev:ui, and dev:back together.
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
4
|
import { existsSync } from "node:fs";
|
|
4
5
|
import { connect as netConnect } from "node:net";
|
|
5
6
|
import { resolve } from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { runDevUi } from "./devUi.mjs";
|
|
8
9
|
import { claimDevSession, stopDevBackContainers } from "./devSession.mjs";
|
|
9
|
-
import { ensureLocalMuscat, GATEWAY_PORT, isPortInUse, robotHasMuscat } from "./muscatLocal.mjs";
|
|
10
|
+
import { ensureLocalMuscat, GATEWAY_PORT, isPortInUse, MUSCAT_CONTAINER, robotHasMuscat } from "./muscatLocal.mjs";
|
|
11
|
+
import { resolveSim, startSim, stopSims } from "./sim.mjs";
|
|
10
12
|
import { assertManifestPorts, assertManifestVariables, readManifest } from "./manifest.mjs";
|
|
11
13
|
import { devPluginId } from "./devId.mjs";
|
|
12
14
|
import {
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
ensureIdeVenv,
|
|
15
17
|
findPluginRoot,
|
|
16
18
|
log,
|
|
19
|
+
parseArgs,
|
|
17
20
|
prompt,
|
|
18
21
|
promptRobotConnection,
|
|
19
22
|
venvPython,
|
|
@@ -129,13 +132,24 @@ export async function runDev(args = []) {
|
|
|
129
132
|
// the robot itself — the local Muscat then publishes the zenoh port for it.
|
|
130
133
|
if (!proxyEndpoint) await ensureRemoteZenohEnv(robotIp, { log });
|
|
131
134
|
|
|
135
|
+
// No robot: a simulator stands in for it (--sim, else the manifest's target).
|
|
136
|
+
const sim = remoteRobot ? null : resolveSim(parseArgs(args).sim, readManifest(root));
|
|
137
|
+
|
|
132
138
|
// A robot running Muscat is the gateway (:10000) and common for both the UI and the backend.
|
|
133
139
|
// Otherwise the local Muscat runtime is; the backend joins its netns, so the plugin's declared
|
|
134
140
|
// ports are published from it too.
|
|
135
141
|
const muscat = (await robotHasMuscat(robotIp)) ? null : await ensureLocalMuscat({
|
|
136
142
|
pluginPorts: portRanges,
|
|
137
143
|
zenohPort: remoteRobot && !proxyEndpoint ? Number(process.env.MUSCAT_ZENOH_PORT) || null : null,
|
|
144
|
+
services: sim?.services,
|
|
145
|
+
env: sim?.env,
|
|
138
146
|
}).catch((e) => { log.warn(`local Muscat: ${e.message}`); return null; });
|
|
147
|
+
if (sim && muscat !== MUSCAT_CONTAINER) {
|
|
148
|
+
log.warn(`--sim needs muscat-pk's own Muscat${muscat ? ` ('${muscat}' is running — stop it to use the simulator)` : ""}; no simulator.`);
|
|
149
|
+
} else if (sim) {
|
|
150
|
+
// dev:ui then serves the real Muscat UI against the local Muscat, like with muscat-dev.
|
|
151
|
+
if (await startSim(sim).catch((e) => log.warn(`simulator: ${e.message}`))) process.env.MUSCAT_PK_SIM = sim.name;
|
|
152
|
+
}
|
|
139
153
|
if (muscat) {
|
|
140
154
|
process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
|
|
141
155
|
} else if (remoteRobot || (await isPortInUse(GATEWAY_PORT))) {
|
|
@@ -160,6 +174,11 @@ export async function runDev(args = []) {
|
|
|
160
174
|
if (hostAgent && hostAgent.exitCode === null) hostAgent.kill();
|
|
161
175
|
if (backend.exitCode === null) backend.kill();
|
|
162
176
|
stopDevBackContainers();
|
|
177
|
+
// Only our own runtime — a muscat-dev the user started stays up.
|
|
178
|
+
if (muscat === MUSCAT_CONTAINER) {
|
|
179
|
+
stopSims();
|
|
180
|
+
spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
|
|
181
|
+
}
|
|
163
182
|
};
|
|
164
183
|
process.once("SIGINT", cleanup);
|
|
165
184
|
process.once("SIGTERM", cleanup);
|
package/cli/src/devBack.mjs
CHANGED
|
@@ -61,7 +61,7 @@ function muscatZenohEnv(container) {
|
|
|
61
61
|
|
|
62
62
|
function backendNetwork(portRanges, robotGateway) {
|
|
63
63
|
const muscat = process.env.MUSCAT_PK_MUSCAT_CONTAINER;
|
|
64
|
-
if (muscat) return { run: ["--network", `container:${muscat}
|
|
64
|
+
if (muscat) return { run: ["--network", `container:${muscat}`, "--ipc", "host"], env: muscatZenohEnv(muscat), shared: true };
|
|
65
65
|
// The robot runs Muscat: its gateway, own netns (host network on Linux), declared ports published.
|
|
66
66
|
if (robotGateway) {
|
|
67
67
|
const run = usesHostNetwork() ? ["--network", "host"] : publishPortFlags(portRanges);
|
package/cli/src/devUi.mjs
CHANGED
|
@@ -214,7 +214,8 @@ export async function runDevUi(args = []) {
|
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
// muscat-dev, or our own Muscat with a simulator (`dev --sim`): a full local Muscat to show.
|
|
218
|
+
const localDevMuscat = !!process.env.MUSCAT_PK_SIM || (await localFullMuscat(robotIp));
|
|
218
219
|
const uiHost = isRemoteRobot(robotIp) ? robotIp : localDevMuscat ? "localhost" : "";
|
|
219
220
|
|
|
220
221
|
const previewRoot = findPreviewHostRoot();
|
package/cli/src/mcp.mjs
CHANGED
|
@@ -39,7 +39,7 @@ import { spawnSync } from "node:child_process";
|
|
|
39
39
|
import { homedir } from "node:os";
|
|
40
40
|
import { join, resolve } from "node:path";
|
|
41
41
|
import { fileURLToPath } from "node:url";
|
|
42
|
-
import { kitRoot, kitVersion, findPluginRoot, venvPython } from "./util.mjs";
|
|
42
|
+
import { dockerUserFlags, kitRoot, kitVersion, findPluginRoot, venvPython } from "./util.mjs";
|
|
43
43
|
import { PYTHON_DEV_IMAGE } from "./python.mjs";
|
|
44
44
|
import { SHIPPED_STORIES_SRC, SHIPPED_DESIGN_TOKENS } from "./storybook.mjs";
|
|
45
45
|
import { CPP_DEV_IMAGE } from "./cpp.mjs";
|
|
@@ -483,14 +483,14 @@ function introspectInDocker(probe) {
|
|
|
483
483
|
const cache = join(cacheRoot, ver || "latest");
|
|
484
484
|
// Only the SDK version in use is kept (~165MB each) — other versions don't pile up.
|
|
485
485
|
for (const old of existsSync(cacheRoot) ? readdirSync(cacheRoot) : []) {
|
|
486
|
-
if (join(cacheRoot, old) !== cache)
|
|
486
|
+
if (join(cacheRoot, old) !== cache) removeCache(join(cacheRoot, old));
|
|
487
487
|
}
|
|
488
488
|
mkdirSync(cache, { recursive: true });
|
|
489
489
|
const pkg = ver ? `rainbow-rb-sdk==${ver}` : "rainbow-rb-sdk";
|
|
490
490
|
const script =
|
|
491
491
|
`[ -f /c/.ok ] || { pip3 install -q --break-system-packages --target /c '${pkg}' >&2 && touch /c/.ok; } && ` +
|
|
492
492
|
`PYTHONPATH=/c python3 -c "$PROBE"`;
|
|
493
|
-
const r = spawnSync("docker", ["run", "--rm", "-v", `${cache}:/c`, "-e", "PROBE", PYTHON_DEV_IMAGE, "bash", "-c", script], {
|
|
493
|
+
const r = spawnSync("docker", ["run", "--rm", ...dockerUserFlags(), "-v", `${cache}:/c`, "-e", "PROBE", PYTHON_DEV_IMAGE, "bash", "-c", script], {
|
|
494
494
|
encoding: "utf8",
|
|
495
495
|
maxBuffer: 8 << 20,
|
|
496
496
|
env: { ...process.env, PROBE: probe },
|
|
@@ -499,6 +499,20 @@ function introspectInDocker(probe) {
|
|
|
499
499
|
return parseProbe(r);
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
+
// Caches written by older kits as root (Linux) can't be unlinked by the user — clear them as root in Docker.
|
|
503
|
+
function removeCache(dir) {
|
|
504
|
+
try {
|
|
505
|
+
rmSync(dir, { recursive: true, force: true });
|
|
506
|
+
} catch {
|
|
507
|
+
spawnSync("docker", ["run", "--rm", "-v", `${dir}:/c`, PYTHON_DEV_IMAGE, "find", "/c", "-mindepth", "1", "-delete"], { stdio: "ignore" });
|
|
508
|
+
try {
|
|
509
|
+
rmSync(dir, { recursive: true, force: true });
|
|
510
|
+
} catch {
|
|
511
|
+
/* left for a later run — must not block reading the SDK */
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
502
516
|
function parseProbe(r) {
|
|
503
517
|
// Importing rainbow.rb_sdk has side effects that print to stdout (e.g. the
|
|
504
518
|
// Zenoh client logs its connection endpoints), ahead of our own final
|
package/cli/src/muscatLocal.mjs
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// preview-host hits http://localhost:3000/common/* (baked into the nexus bundle), so program
|
|
4
4
|
// preview works with no front-end change. The backend container joins its netns. We reuse any
|
|
5
5
|
// Muscat already publishing :10000 (e.g. a `muscat` / `muscat-dev` container) and only otherwise
|
|
6
|
-
// start our own: the S3 runtime image with
|
|
6
|
+
// start our own: the S3 runtime image with `common` (plus the services `--sim` needs).
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { createServer } from "node:net";
|
|
9
9
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import { resolve } from "node:path";
|
|
11
|
+
import { dirname, resolve } from "node:path";
|
|
12
12
|
import { mediaPortFlags, MEDIA_PORTS, publishPortFlags } from "./ports.mjs";
|
|
13
13
|
import { canSkipImageBuild, hasBin, log, buildCacheFlags } from "./util.mjs";
|
|
14
14
|
import { LOCAL_HOSTS } from "./zenohDev.mjs";
|
|
@@ -113,21 +113,40 @@ async function ensureRuntimeImage(total) {
|
|
|
113
113
|
return image;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
async function
|
|
116
|
+
async function ensureServiceBinary(svc, version) {
|
|
117
117
|
const ARCH = dockerArch();
|
|
118
|
-
const dir = resolve(CACHE, "services",
|
|
118
|
+
const dir = resolve(CACHE, "services", svc);
|
|
119
119
|
const stamp = resolve(dir, ".version");
|
|
120
|
-
if (existsSync(stamp) && readFileSync(stamp, "utf8").trim() === version && existsSync(resolve(dir,
|
|
121
|
-
log.step(`downloading Muscat
|
|
122
|
-
const zip = resolve(CACHE,
|
|
123
|
-
writeFileSync(zip, await fetchBuffer(`${S3}
|
|
124
|
-
extractZip(zip, dir, ["config.env",
|
|
120
|
+
if (existsSync(stamp) && readFileSync(stamp, "utf8").trim() === version && existsSync(resolve(dir, `${svc}.${ARCH}.bin`))) return dir;
|
|
121
|
+
log.step(`downloading Muscat ${svc} service ${version} (${ARCH})…`);
|
|
122
|
+
const zip = resolve(CACHE, `${svc}-${version}.zip`);
|
|
123
|
+
writeFileSync(zip, await fetchBuffer(`${S3}/${svc}/${version}.zip`));
|
|
124
|
+
extractZip(zip, dir, ["config.env", `${svc}.${ARCH}.bin`]);
|
|
125
125
|
rmSync(zip, { force: true });
|
|
126
|
-
chmodSync(resolve(dir,
|
|
126
|
+
chmodSync(resolve(dir, `${svc}.${ARCH}.bin`), 0o755);
|
|
127
127
|
writeFileSync(stamp, `${version}\n`);
|
|
128
128
|
return dir;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Sim containers live on this network and connect to common's Zenoh by container name.
|
|
132
|
+
export const MUSCAT_NETWORK = "muscat-pk";
|
|
133
|
+
|
|
134
|
+
function ensureNetwork() {
|
|
135
|
+
if (spawnSync("docker", ["network", "inspect", MUSCAT_NETWORK], { stdio: "ignore" }).status === 0) return;
|
|
136
|
+
spawnSync("docker", ["network", "create", "--label", "com.rainbow-robotics.muscat-pk=true", MUSCAT_NETWORK], { stdio: "ignore" });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// common (127.0.0.1:7447) and the services (127.0.0.1:0) listen on loopback by default; rb-ctl's
|
|
140
|
+
// listen-endpoints file opens both to the network. Sims reach common as muscat-pk-muscat:7447, and
|
|
141
|
+
// gossip has them dial each service back — on loopback only the sim that happened to connect first
|
|
142
|
+
// answers manipulate's receive_one.
|
|
143
|
+
function writeZenohListen(open) {
|
|
144
|
+
const file = resolve(CACHE, "data", "zenoh_listen_endpoints.json5");
|
|
145
|
+
if (!open) return rmSync(file, { force: true });
|
|
146
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
147
|
+
writeFileSync(file, `{\n common: ["tcp/0.0.0.0:7447"],\n service: ["tcp/0.0.0.0:0"],\n}\n`);
|
|
148
|
+
}
|
|
149
|
+
|
|
131
150
|
async function waitForApi(timeoutMs = 180_000) {
|
|
132
151
|
const start = Date.now();
|
|
133
152
|
for (;;) {
|
|
@@ -187,14 +206,23 @@ const containerLabel = (name, label) =>
|
|
|
187
206
|
/**
|
|
188
207
|
* Make sure a Muscat answers on localhost:3000 / :10000. Returns the container name,
|
|
189
208
|
* or null when one can't be started (no Docker, foreign process on the ports).
|
|
190
|
-
*
|
|
209
|
+
* `dev` removes it on exit; `dev:ui` alone leaves it running (`docker rm -f muscat-pk-muscat`).
|
|
191
210
|
*/
|
|
192
|
-
export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } = {}) {
|
|
211
|
+
export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null, services = [], env = {} } = {}) {
|
|
193
212
|
if (process.env.MUSCAT_PK_NO_LOCAL_MUSCAT === "1") return null;
|
|
194
|
-
|
|
213
|
+
// Extra services (log, manipulate, rby1, ...) and env come with --sim; a different set recreates it too.
|
|
214
|
+
const svcs = ["common", ...services.filter((s) => s !== "common")];
|
|
215
|
+
const envFlags = Object.entries(env).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
|
|
216
|
+
const portsKey = [
|
|
217
|
+
...pluginPorts,
|
|
218
|
+
...(zenohPort ? [`zenoh:${zenohPort}`] : []),
|
|
219
|
+
"ipc:host",
|
|
220
|
+
...(services.length ? [`svc:${svcs.join("+")}`] : []),
|
|
221
|
+
...Object.entries(env).map(([k, v]) => `${k}=${v}`),
|
|
222
|
+
].join(",");
|
|
195
223
|
const existing = hasBin("docker") ? localMuscatContainer() : null;
|
|
196
224
|
if (existing === MUSCAT_CONTAINER && containerLabel(existing, PORTS_LABEL) !== portsKey) {
|
|
197
|
-
log.step(`recreating local Muscat
|
|
225
|
+
log.step(`recreating local Muscat for ${portsKey || "no extra ports / services"}`);
|
|
198
226
|
spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
|
|
199
227
|
} else if (existing) {
|
|
200
228
|
if (existing !== MUSCAT_CONTAINER && portsKey)
|
|
@@ -213,11 +241,15 @@ export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } =
|
|
|
213
241
|
|
|
214
242
|
const versions = await resolveVersions();
|
|
215
243
|
const total = versions.total;
|
|
216
|
-
|
|
217
|
-
|
|
244
|
+
if (!total) throw new Error("version.json has no total");
|
|
245
|
+
const missing = svcs.filter((s) => !versions.services?.[s]?.version);
|
|
246
|
+
if (missing.length) throw new Error(`version.json has no services.${missing.join(" / ")}.version`);
|
|
218
247
|
|
|
219
248
|
const image = await ensureRuntimeImage(total);
|
|
220
|
-
const
|
|
249
|
+
const svcMounts = [];
|
|
250
|
+
for (const s of svcs) svcMounts.push("-v", `${await ensureServiceBinary(s, versions.services[s].version)}:/app/services/${s}`);
|
|
251
|
+
ensureNetwork();
|
|
252
|
+
writeZenohListen(services.length > 0);
|
|
221
253
|
|
|
222
254
|
let media = true;
|
|
223
255
|
for (const port of MEDIA_PORTS) if (await isPortInUse(port)) media = false;
|
|
@@ -228,12 +260,16 @@ export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } =
|
|
|
228
260
|
const published = pluginPorts.filter((port) => !busy.includes(port));
|
|
229
261
|
|
|
230
262
|
spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
|
|
231
|
-
log.step(`starting local Muscat (
|
|
263
|
+
log.step(`starting local Muscat (${svcs.map((s) => `${s} ${versions.services[s].version}`).join(", ")}) → http://localhost:${API_PORT}/common/docs`);
|
|
232
264
|
const r = spawnSync("docker", [
|
|
233
265
|
"run", "-d", "--name", MUSCAT_CONTAINER, "--label", "com.rainbow-robotics.muscat-pk=true", "--label", `${PORTS_LABEL}=${portsKey}`,
|
|
234
|
-
"
|
|
266
|
+
"--network", MUSCAT_NETWORK,
|
|
267
|
+
// Like the robot, where plugins run with --ipc host: zenoh SHM and plugin shm_open reach the services.
|
|
268
|
+
"--ipc", "host",
|
|
269
|
+
"-e", `RB_ENABLED_SERVICES=${svcs.join(",")}`,
|
|
235
270
|
"-e", "RB_RUNTIME_ENABLE_ZENOH_BRIDGE=0",
|
|
236
|
-
|
|
271
|
+
...envFlags,
|
|
272
|
+
...svcMounts,
|
|
237
273
|
"-v", `${resolve(CACHE, "data")}:/app/data`,
|
|
238
274
|
"-p", `127.0.0.1:${API_PORT}:${API_PORT}`,
|
|
239
275
|
"-p", `127.0.0.1:${GATEWAY_PORT}:${GATEWAY_PORT}`,
|
|
@@ -245,7 +281,7 @@ export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } =
|
|
|
245
281
|
], { stdio: ["ignore", "ignore", "inherit"] });
|
|
246
282
|
if (r.status !== 0) throw new Error("failed to start the local Muscat container.");
|
|
247
283
|
await waitForApi();
|
|
248
|
-
log.step(
|
|
284
|
+
log.step("local Muscat ready");
|
|
249
285
|
if (published.length) log.step(`plugin backend ports published on 127.0.0.1: ${published.join(", ")}`);
|
|
250
286
|
if (zenohPort) log.step(`Zenoh dev peer port published on :${zenohPort}`);
|
|
251
287
|
return MUSCAT_CONTAINER;
|
|
@@ -69,7 +69,7 @@ export function mount(container, { socketUrl, pluginId, hostServices, data, onSa
|
|
|
69
69
|
// (there may be no backend at all in dev, e.g. no robot reachable) — each rb
|
|
70
70
|
// method awaits this promise internally instead.
|
|
71
71
|
const wsPromise = RbWs.connect(socketUrl, pluginId);
|
|
72
|
-
const rb = createLazyRbSdk(wsPromise, hostServices, { data, onSave });
|
|
72
|
+
const rb = createLazyRbSdk(wsPromise, hostServices, { data, onSave, pluginId });
|
|
73
73
|
const root = createRoot(container);
|
|
74
74
|
let mounted = true;
|
|
75
75
|
const render = (Component) => {
|
package/cli/src/sim.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// `dev --sim <model>`: robot simulators next to the local Muscat, for developing without a robot.
|
|
2
|
+
// Cobot: coreSW (rainbowroboticsofficial/cobot-sim) per component, joined to common's Zenoh.
|
|
3
|
+
// RB-Y1: rby1-sim + Xvfb built locally (it needs a GLFW window; there is no headless mode yet).
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { canSkipImageBuild, log } from "./util.mjs";
|
|
6
|
+
import { MUSCAT_NETWORK } from "./muscatLocal.mjs";
|
|
7
|
+
|
|
8
|
+
const COBOT_IMAGE = "rainbowroboticsofficial/cobot-sim";
|
|
9
|
+
const RBY1_IMAGE = "rainbowroboticsofficial/rby1-sim";
|
|
10
|
+
const RBY1_LOCAL = "muscat-pk-rby1-sim";
|
|
11
|
+
const RBY1_CONTAINER = "muscat-pk-sim-rby1";
|
|
12
|
+
const SIM_LABEL = "com.rainbow-robotics.muscat-pk.sim";
|
|
13
|
+
|
|
14
|
+
// ponytail: model table in code; move to S3 version.json when sims are versioned with Muscat (amr: no sim yet)
|
|
15
|
+
// sim name → coreSW RB_MODEL per component, and the Muscat robot model it registers as.
|
|
16
|
+
const COBOT_MODELS = {
|
|
17
|
+
"500880": { robotModel: "C500880", models: ["500880"] },
|
|
18
|
+
"500920": { robotModel: "C500920", models: ["500920"] },
|
|
19
|
+
RBY2: { robotModel: "RBY2", models: ["C500880", "C501880", "C502880"] },
|
|
20
|
+
};
|
|
21
|
+
const RBY1_TYPES = ["a", "m", "ub"];
|
|
22
|
+
|
|
23
|
+
// rby1-sim renders MuJoCo with software GL every frame; one GL thread and a tiny screen cut it from
|
|
24
|
+
// ~3.5 cores to ~1. The cap keeps a busy Mac usable; the image already skips RT-loop timeouts.
|
|
25
|
+
const RBY1_DOCKERFILE = `ARG BASE=0.10.6-m_v1.3
|
|
26
|
+
FROM ${RBY1_IMAGE}:\${BASE}
|
|
27
|
+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && rm -rf /var/lib/apt/lists/*
|
|
28
|
+
ENV LP_NUM_THREADS=0 DISPLAY=:99
|
|
29
|
+
CMD ["sh", "-c", "Xvfb :99 -screen 0 320x240x16 -nolisten tcp & sleep 1; exec ./app_main"]
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
export const SIM_NAMES = [...Object.keys(COBOT_MODELS), ...RBY1_TYPES.map((t) => `rby1-${t}`)];
|
|
33
|
+
|
|
34
|
+
/** `muscat-pk sim list`: "<name>\t<what runs>" per line, for scripts and the VS Code Dev picker. */
|
|
35
|
+
export function runSimList() {
|
|
36
|
+
for (const [name, { robotModel, models }] of Object.entries(COBOT_MODELS))
|
|
37
|
+
console.log(`${name}\tcobot · ${robotModel}${models.length > 1 ? ` (${models.join(", ")})` : ""}`);
|
|
38
|
+
for (const t of RBY1_TYPES) console.log(`rby1-${t}\tRB-Y1 ${t.toUpperCase()}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** "500880" | "RBY2" | "rby1-m" | "rby1-m_v1.2" → { services, env, ... } or null. */
|
|
42
|
+
export function parseSim(name) {
|
|
43
|
+
if (COBOT_MODELS[name]) return { name, kind: "cobot", services: ["log", "manipulate"], env: {}, ...COBOT_MODELS[name] };
|
|
44
|
+
const m = /^rby1-(a|m|ub)(?:_(v[\d.]+))?$/.exec(name ?? "");
|
|
45
|
+
if (m) return { name, kind: "rby1", services: ["log", "rby1"], env: { RBY1_HOST: RBY1_CONTAINER }, robotModel: m[1], type: m[1], version: m[2] };
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Only an explicit --sim <name> runs a simulator. */
|
|
50
|
+
export function resolveSim(simArg, manifest) {
|
|
51
|
+
if (simArg === undefined) return null;
|
|
52
|
+
const sim = typeof simArg === "string" ? parseSim(simArg) : null;
|
|
53
|
+
if (!sim) throw new Error(`--sim takes one of: ${SIM_NAMES.join(", ")} (rby1 also takes rby1-m_v1.2)`);
|
|
54
|
+
const models = Array.isArray(manifest.targetModels) ? manifest.targetModels : null;
|
|
55
|
+
if (models && !models.includes(simArg)) log.warn(`--sim ${simArg} is not in manifest targetModels (${models.join(", ")})`);
|
|
56
|
+
return sim;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function hubTags(repo) {
|
|
60
|
+
const res = await fetch(`https://hub.docker.com/v2/repositories/${repo}/tags?page_size=100&ordering=last_updated`, {
|
|
61
|
+
signal: AbortSignal.timeout(10_000),
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) throw new Error(`Docker Hub ${repo}: HTTP ${res.status}`);
|
|
64
|
+
return (await res.json()).results.map((t) => t.name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// "0.10.6-m_v1.3" → [0, 10, 6, 1, 3]
|
|
68
|
+
const semver = (v) => v.split(/[._v-]/).map(Number).filter((n) => !Number.isNaN(n));
|
|
69
|
+
const newer = (a, b) => {
|
|
70
|
+
const [x, y] = [semver(a), semver(b)];
|
|
71
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) - (y[i] ?? 0);
|
|
72
|
+
return 0;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Latest published tag; offline falls back to whatever was pulled before.
|
|
76
|
+
async function latestTag(repo, pattern) {
|
|
77
|
+
try {
|
|
78
|
+
const tags = (await hubTags(repo)).filter((t) => pattern.test(t));
|
|
79
|
+
if (tags.length) return tags.sort(newer).at(-1);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
log.warn(`${e.message} — using a local image`);
|
|
82
|
+
}
|
|
83
|
+
const local = spawnSync("docker", ["images", repo, "--format", "{{.Tag}}"], { encoding: "utf8" }).stdout ?? "";
|
|
84
|
+
const tags = local.split("\n").map((t) => t.trim()).filter((t) => pattern.test(t));
|
|
85
|
+
if (!tags.length) throw new Error(`no ${repo} image matching ${pattern} (offline and nothing pulled yet)`);
|
|
86
|
+
return tags.sort(newer).at(-1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pull(image) {
|
|
90
|
+
if (spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" }).status === 0) return;
|
|
91
|
+
log.step(`docker pull ${image}…`);
|
|
92
|
+
if (spawnSync("docker", ["pull", image], { stdio: "inherit" }).status !== 0) throw new Error(`docker pull ${image} failed`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function run(name, args) {
|
|
96
|
+
spawnSync("docker", ["rm", "-f", name], { stdio: "ignore" });
|
|
97
|
+
const r = spawnSync("docker", [
|
|
98
|
+
// --ipc host like the robot: the cobot coreSWs share one shm segment (RB_MESSAGE_SHM, a slot per model).
|
|
99
|
+
"run", "-d", "--name", name, "--network", MUSCAT_NETWORK, "--ipc", "host",
|
|
100
|
+
"--label", "com.rainbow-robotics.muscat-pk=true", "--label", `${SIM_LABEL}=true`,
|
|
101
|
+
...args,
|
|
102
|
+
], { stdio: ["ignore", "ignore", "inherit"] });
|
|
103
|
+
if (r.status !== 0) throw new Error(`failed to start ${name}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The Muscat UI (robot info, components, connection state) follows the registered robot model.
|
|
107
|
+
async function registerRobot(robotModel) {
|
|
108
|
+
const res = await fetch("http://127.0.0.1:3000/common/upsert-robot-info", {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ robotModel }),
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok) throw new Error(`upsert-robot-info ${robotModel}: HTTP ${res.status}`);
|
|
114
|
+
log.step(`robot model ${robotModel} registered in the local Muscat`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Start the simulator containers and register the robot model; returns the container names. */
|
|
118
|
+
export async function startSim(sim) {
|
|
119
|
+
const names = await startContainers(sim);
|
|
120
|
+
await registerRobot(sim.robotModel);
|
|
121
|
+
return names;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Needs the local Muscat on MUSCAT_NETWORK.
|
|
125
|
+
async function startContainers(sim) {
|
|
126
|
+
stopSims();
|
|
127
|
+
if (sim.kind === "cobot") {
|
|
128
|
+
const image = `${COBOT_IMAGE}:${await latestTag(COBOT_IMAGE, /^\d+\.\d+\.\d+$/)}`;
|
|
129
|
+
pull(image);
|
|
130
|
+
const names = sim.models.map((model) => `muscat-pk-sim-${model}`);
|
|
131
|
+
sim.models.forEach((model, i) => run(names[i], [
|
|
132
|
+
"--cap-add", "SYS_NICE", "--ulimit", "rtprio=99",
|
|
133
|
+
"-e", `RB_MODEL=${model}`,
|
|
134
|
+
"-e", "RB_ZENOH_CONNECT=tcp/muscat-pk-muscat:7447",
|
|
135
|
+
image,
|
|
136
|
+
]));
|
|
137
|
+
log.step(`cobot sim ${sim.name} (${image}) → ${sim.models.join(", ")}`);
|
|
138
|
+
return names;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const pattern = sim.version ? new RegExp(`^\\d+\\.\\d+\\.\\d+-${sim.type}_${sim.version.replace(/\./g, "\\.")}$`) : new RegExp(`^\\d+\\.\\d+\\.\\d+-${sim.type}_v[\\d.]+$`);
|
|
142
|
+
const base = await latestTag(RBY1_IMAGE, pattern);
|
|
143
|
+
const image = `${RBY1_LOCAL}:${base}`;
|
|
144
|
+
if (!canSkipImageBuild(image)) {
|
|
145
|
+
log.step(`building ${image} (rby1-sim + Xvfb, first run pulls ~150MB)…`);
|
|
146
|
+
const r = spawnSync("docker", ["build", "-t", image, "--build-arg", `BASE=${base}`, "--label", "com.rainbow-robotics.muscat-pk=true", "-"], {
|
|
147
|
+
input: RBY1_DOCKERFILE,
|
|
148
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
149
|
+
});
|
|
150
|
+
if (r.status !== 0) throw new Error(`docker build ${image} failed`);
|
|
151
|
+
}
|
|
152
|
+
run(RBY1_CONTAINER, ["--cpus", process.env.MUSCAT_PK_RBY1_CPUS || "1.5", image]);
|
|
153
|
+
log.step(`rby1 sim ${base} → ${RBY1_CONTAINER}:50051`);
|
|
154
|
+
return [RBY1_CONTAINER];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function stopSims() {
|
|
158
|
+
const ids = (spawnSync("docker", ["ps", "-aq", "--filter", `label=${SIM_LABEL}=true`], { encoding: "utf8" }).stdout ?? "").split("\n").filter(Boolean);
|
|
159
|
+
if (!ids.length) return;
|
|
160
|
+
// The cobot coreSWs share these under --ipc host; drop them so the next run starts from a fresh segment.
|
|
161
|
+
spawnSync("docker", ["exec", ids[0], "sh", "-c", "rm -f /dev/shm/RB_COBOT_SHM /dev/shm/RB_MESSAGE_SHM"], { stdio: "ignore" });
|
|
162
|
+
spawnSync("docker", ["rm", "-f", ...ids], { stdio: "ignore" });
|
|
163
|
+
}
|
package/cli/src/util.mjs
CHANGED
|
@@ -144,6 +144,8 @@ const LOCAL_HOSTS = new Set(["", "localhost", "127.0.0.1", "::1"]);
|
|
|
144
144
|
*/
|
|
145
145
|
export async function promptRobotConnection(args = []) {
|
|
146
146
|
const parsed = parseArgs(args);
|
|
147
|
+
// A simulator stands in for the robot.
|
|
148
|
+
if (parsed.sim !== undefined) return { robotIp: "", robotToken: "" };
|
|
147
149
|
const robotIp =
|
|
148
150
|
parsed["robot-ip"] ??
|
|
149
151
|
process.env.MUSCAT_ROBOT_HOST ??
|