@rainbow-robotics/muscat-plugin-kit 1.10.1-dev.1 → 1.10.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 +282 -282
- package/cli/README.md +274 -274
- package/cli/bin/create-muscat-plugin.mjs +8 -8
- package/cli/bin/muscat-pk.mjs +155 -155
- package/cli/bundle-templates.mjs +31 -31
- package/cli/docker/Dockerfile.cpp +28 -28
- package/cli/docker/Dockerfile.dev +30 -30
- package/cli/docker/Dockerfile.python +21 -21
- package/cli/m5-e2e.mjs +69 -69
- package/cli/prepack.mjs +10 -10
- package/cli/src/ai-rules.mjs +70 -70
- package/cli/src/build.mjs +447 -447
- package/cli/src/check-package.mjs +13 -13
- package/cli/src/completion.mjs +166 -166
- package/cli/src/cpp.mjs +78 -78
- package/cli/src/cppIde.mjs +75 -75
- package/cli/src/dev.mjs +165 -165
- package/cli/src/devBack.mjs +734 -727
- package/cli/src/devUi.mjs +247 -243
- package/cli/src/esbuild.mjs +56 -56
- package/cli/src/host_agent.py +223 -223
- package/cli/src/manifest.mjs +287 -287
- package/cli/src/mcp.mjs +1071 -1071
- package/cli/src/muscatLocal.mjs +209 -209
- package/cli/src/new.mjs +343 -343
- package/cli/src/pluginDevServer.mjs +111 -111
- package/cli/src/ports.mjs +193 -193
- package/cli/src/python.mjs +112 -112
- package/cli/src/ros2.mjs +109 -109
- package/cli/src/sdk.mjs +117 -117
- package/cli/src/storybook.mjs +168 -168
- package/cli/src/type.mjs +141 -141
- package/cli/src/uninstall.mjs +81 -81
- package/cli/src/update.mjs +295 -295
- package/cli/src/util.mjs +441 -441
- package/cli/src/zenohDev.mjs +83 -81
- package/cli/storybook-dist/_design-tokens.css +4957 -4957
- package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -46
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -73
- package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -41
- package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -60
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -44
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -56
- package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -55
- package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -52
- package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -108
- package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -106
- package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -65
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -68
- package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -216
- package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -90
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -31
- package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -36
- package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -116
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -99
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -123
- package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -135
- package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -69
- package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -91
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -38
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -112
- package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -75
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -78
- package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -177
- package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -117
- package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -30
- package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -54
- package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -103
- package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -67
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -46
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-D6JqZzDJ.js → Color-YHDXOIA2-B4IER17l.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-ESb7JAHg.js → Docs-C8GCHFtK.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-6gD2kUo1.js → Docs-CgdEMPQP.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-B9wd8Ok7.js → Docs-Ck3WHvk6.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-CgFbWWXM.js → Docs-D9NHsoeZ.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-DBQup8Bu.js → Docs-DNYQC7nl.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-CMjovnSY.js → Docs-DUfeiM4I.js} +4 -4
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-BOUI1AHi.js → DocsRenderer-CFRXHY34-C0WhyDoR.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-DsYFMUh3.js → NxAvatar.stories-CwnK8AUF.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-CHfNcqsc.js → NxButton.stories-ueYdosfQ.js} +8 -8
- package/cli/storybook-dist/assets/{NxNumberInput.stories-BFw_4vFE.js → NxNumberInput.stories-M5ZCLbrK.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-DVfxyw4M.js → NxPagination.stories-bxxUnHEL.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BzyxUWoZ.js → NxPointerHoldButton.stories-BHxB2kr5.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-DmRdzBc1.js → NxRangeInput.stories-BKfr-fp4.js} +6 -6
- package/cli/storybook-dist/assets/{NxSelectBox.stories-B1KkNUOf.js → NxSelectBox.stories-CG8FuTBq.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CyOdvfQ6.js → NxSlideRange.stories-BRTFtMTL.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-DrDBw0qp.js → NxSwitch.stories-Bwuyzn-R.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-DTB2aqNS.js → NxTextArea.stories-COq2sGqu.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-DU0rucmM.js → NxTextInput.stories-CjsBLz05.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-DTfOtXtp.js → RBAccordion.stories-BARwuZ0Q.js} +3 -3
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-9y9BjrUe.js → RBAccordionGroup.stories-COrPxETW.js} +10 -10
- package/cli/storybook-dist/assets/{RBAlert.stories-DLTAv9fT.js → RBAlert.stories-BQrkjZPt.js} +3 -3
- package/cli/storybook-dist/assets/{RBBadge.stories-BNMACewA.js → RBBadge.stories-BTsLL-lK.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-B3kXMAt5.js → RBBattery.stories-DAeQdJbc.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-Y-s8R2n7.js → RBCard.stories-HyhOE80R.js} +39 -39
- package/cli/storybook-dist/assets/{RBCheckBox.stories-DBuT9tL4.js → RBCheckBox.stories-KFk0WqX_.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CWu1TL56.js → RBCheckIconButton.stories-CXbbktF5.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-sCu2MGCX.js → RBCheckLabelButton.stories-DWfYz_9Z.js} +17 -17
- package/cli/storybook-dist/assets/{RBChipTab.stories-BX9qLJkR.js → RBChipTab.stories-JPEgVFAS.js} +5 -5
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-35u1aJ-P.js → RBChipsBoxTab.stories-CTPuWXrQ.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-DAOZ5Ies.js → RBColorIcons.stories-DiFRZ5jk.js} +1 -1
- package/cli/storybook-dist/assets/RBCommand.stories-Pf1HAVIR.js +73 -0
- package/cli/storybook-dist/assets/{RBConfirm.stories-iMXcUh7Z.js → RBConfirm.stories-D1cEPUXk.js} +3 -3
- package/cli/storybook-dist/assets/{RBDateInput.stories-hK5rXECf.js → RBDateInput.stories-RPYpCjig.js} +1 -1
- package/cli/storybook-dist/assets/RBDialog.stories-DJH-0XSR.js +38 -0
- package/cli/storybook-dist/assets/{RBDivider.stories-BAK-iZ1Z.js → RBDivider.stories-uEj1vOgK.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-B2nqf2Nh.js → RBDrawer.stories-BiMzElb2.js} +12 -12
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-Da_TBkla.js +86 -0
- package/cli/storybook-dist/assets/{RBEmptyView.stories-nCenrgBh.js → RBEmptyView.stories-BHeKGfPS.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-DRH7O3sp.js → RBIconButton.stories-NEsRFoYp.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-D3PyJXG8.js → RBIcons.stories-CcZ-TTLS.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-modQbt10.js → RBInlineLabelButton.stories-B0wpdQyR.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-BY14qVgR.js → RBJoyStick.stories-DEgBou3K.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-DzF7vGwm.js → RBLabelButton.stories-DT8drIsF.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-lF4FRmVh.js → RBLight.stories-Dh-aoeyG.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-XhrI7bsV.js → RBMenubar.stories-D-H7UqVh.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DprllQDl.js → RBMultiRangeInput.stories-hlclv6jI.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-2v2gBJfC.js → RBPagination.stories-DWCeoZ_S.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-C8iLIZpv.js → RBPopover.stories-C514M09_.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-Th_HrWHI.js → RBPrompt.stories-BSItvfiT.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-CoHuLaN3.js → RBRadioBox.stories-BBb4YbFU.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-DULxE_ff.js → RBRadioButton.stories-CXyOGJCv.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-D8A9KBSS.js → RBRangeDateInput.stories-qvhubu0e.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-C41QfrQ3.js → RBResizable.stories-CZKeyGjH.js} +14 -14
- package/cli/storybook-dist/assets/{RBSelectBox.stories-C1gNmwzw.js → RBSelectBox.stories-CRKP3ybf.js} +1 -1
- package/cli/storybook-dist/assets/RBSheet.stories-sCM3ZJUa.js +39 -0
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-9LlgQ8nh.js → RBSideNumberInput.stories-Bb7gw8RF.js} +3 -3
- package/cli/storybook-dist/assets/{RBSlider.stories-oUgo5K9d.js → RBSlider.stories-23OAQU5z.js} +3 -3
- package/cli/storybook-dist/assets/{RBSpinner.stories-DDpohbY2.js → RBSpinner.stories-2xcQdcmt.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-gW-GN7QO.js → RBSwitch.stories-BACUaJm_.js} +7 -7
- package/cli/storybook-dist/assets/{RBTab.stories-CN-PV3HH.js → RBTab.stories-CDHE3sVs.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-BAZLpxtM.js → RBTable.stories-C0Cfhfl7.js} +29 -29
- package/cli/storybook-dist/assets/{RBTableRow.stories-CwjVrszB.js → RBTableRow.stories-BF8kIkpp.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-JuEo2sUL.js → RBTabs.stories-BdJWRq1o.js} +7 -7
- package/cli/storybook-dist/assets/{RBTextArea.stories-D0_Pv4qZ.js → RBTextArea.stories-CGy2xeYH.js} +3 -3
- package/cli/storybook-dist/assets/{RBTextField.stories-CCsOvZqV.js → RBTextField.stories-IfUOzxCM.js} +15 -15
- package/cli/storybook-dist/assets/{RBTextInput.stories-Cccn3VNm.js → RBTextInput.stories-VjXNFo4R.js} +3 -3
- package/cli/storybook-dist/assets/{RBToast.stories-I5617t0K.js → RBToast.stories-BzcobblV.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-DsgKjGsJ.js → RBToggle.stories-DuLE5mKv.js} +4 -4
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-BZEzSDuz.js → RBToggleGroup.stories-CuR0N0GJ.js} +5 -5
- package/cli/storybook-dist/assets/{RBTooltip.stories-4oSyAWsA.js → RBTooltip.stories--aAgNC8_.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-DSKbd0bG.js → SideNumberInput-DyWQxYUj.js} +743 -743
- package/cli/storybook-dist/assets/{entry-preview-B35XREGs.js → entry-preview-D7IHsza_.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-BB797lmL.js → iframe-CbaIp5I-.js} +2 -2
- package/cli/storybook-dist/assets/{index-BU2567EP.js → index-C6CBgMuG.js} +3 -3
- package/cli/storybook-dist/assets/{preview-C7DaSWyc.js → preview-B-iYLDuW.js} +2 -2
- package/cli/storybook-dist/assets/{preview-CbuRHit4.js → preview-CRWqc9DH.js} +1 -1
- package/cli/storybook-dist/assets/{react-resizable-panels.browser-ChIvlwo7.js → react-resizable-panels.browser-8hC8dr-6.js} +8 -8
- package/cli/storybook-dist/iframe.html +2 -2
- package/cli/storybook-dist/index.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/.mcp.json +8 -8
- package/cli/templates/common/README.md +1231 -1231
- package/cli/templates/common/assets/icon.svg +45 -45
- package/cli/templates/common/manifest.json +26 -26
- package/cli/templates/common/package.json +22 -22
- package/cli/templates/common/py/main.py +125 -125
- package/cli/templates/common/tsconfig.json +13 -13
- package/cli/templates/common/ui/content.tsx +216 -216
- package/cli/templates/common/ui/style.css +6 -6
- package/cli/templates/common/vite.config.ts +10 -10
- package/cli/templates/cpp/cpp/main.cpp +73 -73
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -25
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -10
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -18
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -11
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +23 -23
- package/cli/vite.d.mts +25 -25
- package/cli/vite.mjs +292 -292
- package/package.json +3 -3
- package/cli/storybook-dist/assets/RBCommand.stories-BSqXDkKw.js +0 -73
- package/cli/storybook-dist/assets/RBDialog.stories-BAktz-ku.js +0 -38
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-DbW2fWAj.js +0 -86
- package/cli/storybook-dist/assets/RBSheet.stories-_6VH-RUm.js +0 -39
package/cli/src/devBack.mjs
CHANGED
|
@@ -1,727 +1,734 @@
|
|
|
1
|
-
// muscat-pk dev:back — run the plugin back end during development.
|
|
2
|
-
// Every backend runs in a warm Docker container that matches the robot
|
|
3
|
-
// (Ubuntu 24.04), so dev behaves like production.
|
|
4
|
-
// python : Python 3.12 container, pip install, run main.py with watchfiles
|
|
5
|
-
// hot-reload. Shares the local Muscat container's netns (localhost:10000).
|
|
6
|
-
// cpp : Ubuntu + cmake container, build cpp/, run the binary, rebuild +
|
|
7
|
-
// restart on source changes.
|
|
8
|
-
// ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
|
|
9
|
-
// on source changes (rclcpp rebuilds on change).
|
|
10
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
|
|
12
|
-
import { isIP } from "node:net";
|
|
13
|
-
import { dirname, resolve } from "node:path";
|
|
14
|
-
import { dockerUserFlags, ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
|
|
15
|
-
import { ensureRemoteZenohEnv, getFreePort, isRemoteRobot, usesHostNetwork } from "./zenohDev.mjs";
|
|
16
|
-
import { GATEWAY_PORT, localMuscatContainer, robotHasMuscat } from "./muscatLocal.mjs";
|
|
17
|
-
import { PLUGIN_ID_RE, assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, rosNamespace, validateManifest } from "./manifest.mjs";
|
|
18
|
-
import {
|
|
19
|
-
portEnvFlags,
|
|
20
|
-
publishPortFlags,
|
|
21
|
-
startPortContractMonitor,
|
|
22
|
-
} from "./ports.mjs";
|
|
23
|
-
import {
|
|
24
|
-
PYTHON_DEV_IMAGE,
|
|
25
|
-
assertDockerReady as pyAssertDockerReady,
|
|
26
|
-
ensureImage as pyEnsureImage,
|
|
27
|
-
} from "./python.mjs";
|
|
28
|
-
import {
|
|
29
|
-
ROS2_DEV_IMAGE,
|
|
30
|
-
assertDockerReady,
|
|
31
|
-
ensureImage,
|
|
32
|
-
ros2PackageName,
|
|
33
|
-
} from "./ros2.mjs";
|
|
34
|
-
import {
|
|
35
|
-
CPP_DEV_IMAGE,
|
|
36
|
-
assertDockerReady as cppAssertDockerReady,
|
|
37
|
-
ensureImage as cppEnsureImage,
|
|
38
|
-
} from "./cpp.mjs";
|
|
39
|
-
|
|
40
|
-
const MANAGED_DEV_ENV = "MUSCAT_PK_MANAGED_DEV";
|
|
41
|
-
|
|
42
|
-
function exitAfterCleanup(code = 0) {
|
|
43
|
-
if (process.env[MANAGED_DEV_ENV] !== "1") process.exit(code ?? 0);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function stopDevBackContainers() {
|
|
47
|
-
for (const container of [
|
|
48
|
-
`${PYTHON_DEV_IMAGE}-run`,
|
|
49
|
-
`${ROS2_DEV_IMAGE}-run`,
|
|
50
|
-
`${CPP_DEV_IMAGE}-run`,
|
|
51
|
-
]) {
|
|
52
|
-
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Where the backend's 127.0.0.1 points. A local Muscat container (muscat-pk-muscat, muscat-dev):
|
|
57
|
-
// join its netns so the gateway and MediaMTX resolve like on the robot. Otherwise its own
|
|
58
|
-
// netns, gateway via the host.
|
|
59
|
-
// IS_DEV Muscat (muscat-dev): common is a zenoh-router client and opens no 7447 listener,
|
|
60
|
-
// so the plugin must join the router too or `execute` queries never reach it.
|
|
61
|
-
function muscatZenohEnv(container) {
|
|
62
|
-
const r = spawnSync("docker", ["inspect", "-f", "{{range .Config.Env}}{{println .}}{{end}}", container], { encoding: "utf8" });
|
|
63
|
-
return /^IS_DEV=(true|1)$/im.test(r.stdout || "")
|
|
64
|
-
? ["-e", "ZENOH_PEER_CONNECT_ENDPOINTS=tcp/zenoh-router:7447"]
|
|
65
|
-
: [];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function backendNetwork(portRanges, robotGateway) {
|
|
69
|
-
const muscat = process.env.MUSCAT_PK_MUSCAT_CONTAINER;
|
|
70
|
-
if (muscat) return { run: ["--network", `container:${muscat}`], env: muscatZenohEnv(muscat), shared: true };
|
|
71
|
-
// The robot runs Muscat: its gateway, own netns (host network on Linux), declared ports published.
|
|
72
|
-
if (robotGateway) {
|
|
73
|
-
const run = usesHostNetwork() ? ["--network", "host"] : publishPortFlags(portRanges);
|
|
74
|
-
return { run, env: ["-e", `RB_PLUGIN_GATEWAY_URL=${robotGateway}`], shared: usesHostNetwork() };
|
|
75
|
-
}
|
|
76
|
-
if (usesHostNetwork()) return { run: ["--network", "host"], env: [], shared: true };
|
|
77
|
-
return {
|
|
78
|
-
// host.docker.internal is automatic on Docker Desktop; this makes it resolve on Linux too.
|
|
79
|
-
run: [...publishPortFlags(portRanges), "--add-host", "host.docker.internal:host-gateway"],
|
|
80
|
-
env: ["-e", "RB_PLUGIN_GATEWAY_URL=http://host.docker.internal:10000"],
|
|
81
|
-
shared: false,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// rb.rosPeers in dev: answer muscat/plugin/<id>/ros_peers/{get,set} on the gateway the way
|
|
86
|
-
// Muscat (common) does on the robot — keep the list in .muscat-dev/ and restart the node
|
|
87
|
-
// with ROS_STATIC_PEERS.
|
|
88
|
-
const ROS_PEERS_FILE = ".muscat-dev/ros-peers.json";
|
|
89
|
-
|
|
90
|
-
function readRosPeers(root) {
|
|
91
|
-
try {
|
|
92
|
-
return JSON.parse(readFileSync(resolve(root, ROS_PEERS_FILE), "utf8"));
|
|
93
|
-
} catch {
|
|
94
|
-
return [];
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function serveRosPeers(root, pluginId, restart) {
|
|
99
|
-
if (!pluginId) return;
|
|
100
|
-
const { RbWs } = await import("@rainbow-robotics/plugin-ui/ws");
|
|
101
|
-
const ws = await Promise.race([
|
|
102
|
-
RbWs.connect("http://127.0.0.1:10000", pluginId).catch(() => null),
|
|
103
|
-
new Promise((r) => setTimeout(() => r(null), 5000)),
|
|
104
|
-
]);
|
|
105
|
-
if (!ws) return log.warn("rb.rosPeers unavailable in dev: plugin gateway not reachable on :10000");
|
|
106
|
-
const base = `/muscat/plugin/${pluginId}/ros_peers`;
|
|
107
|
-
await ws.serve(`${base}/get`, () => ({ peers: readRosPeers(root) }));
|
|
108
|
-
await ws.serve(`${base}/set`, (payload) => {
|
|
109
|
-
const peers = payload?.peers;
|
|
110
|
-
if (!Array.isArray(peers) || !peers.every((p) => typeof p === "string" && isIP(p))) {
|
|
111
|
-
return { ok: false, error: "peers must be a list of IP addresses" };
|
|
112
|
-
}
|
|
113
|
-
const file = resolve(root, ROS_PEERS_FILE);
|
|
114
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
115
|
-
writeFileSync(file, JSON.stringify(peers) + "\n");
|
|
116
|
-
log.step(`ROS_STATIC_PEERS → ${peers.join(";") || "(none)"} — restarting node`);
|
|
117
|
-
restart();
|
|
118
|
-
return { ok: true, peers };
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── IDE tooling venv (best-effort, non-blocking) ────────────────────────────
|
|
123
|
-
// `muscat-pk new` already creates this up front (see new.mjs); this just
|
|
124
|
-
// refreshes it on every `dev`/`dev:back` in case requirements.txt changed.
|
|
125
|
-
// Docker remains the actual execution environment — this never affects it.
|
|
126
|
-
// Runs in the background so it never delays `dev:back` starting, and any
|
|
127
|
-
// failure here (no local Python toolchain, offline, ...) is silent.
|
|
128
|
-
function syncIdeVenv(root) {
|
|
129
|
-
setImmediate(() => {
|
|
130
|
-
try {
|
|
131
|
-
ensureIdeVenv(root);
|
|
132
|
-
} catch {
|
|
133
|
-
/* best-effort only */
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── python ────────────────────────────────────────────────────────────────────
|
|
139
|
-
// Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
|
|
140
|
-
// Under `muscat-pk dev`, it shares the local Muscat container's netns, so the SDK
|
|
141
|
-
// reaches the plugin gateway at the default localhost:10000.
|
|
142
|
-
async function runPython(root, portRanges, robotGateway) {
|
|
143
|
-
// Independent of Docker readiness — must still happen if Docker isn't
|
|
144
|
-
// installed/running, since that's exactly when local IDE tooling matters most.
|
|
145
|
-
syncIdeVenv(root);
|
|
146
|
-
pyAssertDockerReady();
|
|
147
|
-
await pyEnsureImage();
|
|
148
|
-
|
|
149
|
-
const manifest = readManifest(root);
|
|
150
|
-
const pluginId = manifest.id ?? "";
|
|
151
|
-
const container = `${PYTHON_DEV_IMAGE}-run`;
|
|
152
|
-
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
153
|
-
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
154
|
-
const zenohAdvertiseHost = process.env.MUSCAT_ZENOH_ADVERTISE_HOST ?? "";
|
|
155
|
-
const zenohPort = process.env.MUSCAT_ZENOH_PORT ?? "";
|
|
156
|
-
// See host_agent.py: a Docker Desktop container can't join the robot's
|
|
157
|
-
// Zenoh mesh, so it speaks a byte protocol to the host-side proxy instead.
|
|
158
|
-
const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT ?? "";
|
|
159
|
-
// Native Linux (not WSL, not Docker Desktop) has real host networking: the
|
|
160
|
-
// container shares the host's network stack, so it reaches the gateway at
|
|
161
|
-
// localhost:10000 and the robot's Zenoh router / reverse-dial path directly
|
|
162
|
-
// — no netns sharing, port publishing, host.docker.internal, or byte-proxy.
|
|
163
|
-
const net = backendNetwork(portRanges, robotGateway);
|
|
164
|
-
// Only the container's OWN reverse-dial path needs to publish a listen port.
|
|
165
|
-
// With a shared netns the port is already reachable; with the byte-proxy
|
|
166
|
-
// the container just connects out — neither needs -p.
|
|
167
|
-
const standaloneZenohPortFlags =
|
|
168
|
-
!net.shared && !proxyEndpoint && zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : [];
|
|
169
|
-
// C++ SDK (rainbow-rb-sdk >= 0.10) can't use the byte shim: its Zenoh session
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
...(
|
|
185
|
-
...(
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
...
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
docker(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
while len(
|
|
294
|
-
chunk = conn.recv(
|
|
295
|
-
if not chunk:
|
|
296
|
-
return None
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
self.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
self.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
self._sub_by_keyexpr
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
def
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
child
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
};
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1
|
+
// muscat-pk dev:back — run the plugin back end during development.
|
|
2
|
+
// Every backend runs in a warm Docker container that matches the robot
|
|
3
|
+
// (Ubuntu 24.04), so dev behaves like production.
|
|
4
|
+
// python : Python 3.12 container, pip install, run main.py with watchfiles
|
|
5
|
+
// hot-reload. Shares the local Muscat container's netns (localhost:10000).
|
|
6
|
+
// cpp : Ubuntu + cmake container, build cpp/, run the binary, rebuild +
|
|
7
|
+
// restart on source changes.
|
|
8
|
+
// ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
|
|
9
|
+
// on source changes (rclcpp rebuilds on change).
|
|
10
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
|
|
12
|
+
import { isIP } from "node:net";
|
|
13
|
+
import { dirname, resolve } from "node:path";
|
|
14
|
+
import { dockerUserFlags, ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
|
|
15
|
+
import { ensureRemoteZenohEnv, getFreePort, isRemoteRobot, usesHostNetwork } from "./zenohDev.mjs";
|
|
16
|
+
import { GATEWAY_PORT, localMuscatContainer, robotHasMuscat } from "./muscatLocal.mjs";
|
|
17
|
+
import { PLUGIN_ID_RE, assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, rosNamespace, validateManifest } from "./manifest.mjs";
|
|
18
|
+
import {
|
|
19
|
+
portEnvFlags,
|
|
20
|
+
publishPortFlags,
|
|
21
|
+
startPortContractMonitor,
|
|
22
|
+
} from "./ports.mjs";
|
|
23
|
+
import {
|
|
24
|
+
PYTHON_DEV_IMAGE,
|
|
25
|
+
assertDockerReady as pyAssertDockerReady,
|
|
26
|
+
ensureImage as pyEnsureImage,
|
|
27
|
+
} from "./python.mjs";
|
|
28
|
+
import {
|
|
29
|
+
ROS2_DEV_IMAGE,
|
|
30
|
+
assertDockerReady,
|
|
31
|
+
ensureImage,
|
|
32
|
+
ros2PackageName,
|
|
33
|
+
} from "./ros2.mjs";
|
|
34
|
+
import {
|
|
35
|
+
CPP_DEV_IMAGE,
|
|
36
|
+
assertDockerReady as cppAssertDockerReady,
|
|
37
|
+
ensureImage as cppEnsureImage,
|
|
38
|
+
} from "./cpp.mjs";
|
|
39
|
+
|
|
40
|
+
const MANAGED_DEV_ENV = "MUSCAT_PK_MANAGED_DEV";
|
|
41
|
+
|
|
42
|
+
function exitAfterCleanup(code = 0) {
|
|
43
|
+
if (process.env[MANAGED_DEV_ENV] !== "1") process.exit(code ?? 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function stopDevBackContainers() {
|
|
47
|
+
for (const container of [
|
|
48
|
+
`${PYTHON_DEV_IMAGE}-run`,
|
|
49
|
+
`${ROS2_DEV_IMAGE}-run`,
|
|
50
|
+
`${CPP_DEV_IMAGE}-run`,
|
|
51
|
+
]) {
|
|
52
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Where the backend's 127.0.0.1 points. A local Muscat container (muscat-pk-muscat, muscat-dev):
|
|
57
|
+
// join its netns so the gateway and MediaMTX resolve like on the robot. Otherwise its own
|
|
58
|
+
// netns, gateway via the host.
|
|
59
|
+
// IS_DEV Muscat (muscat-dev): common is a zenoh-router client and opens no 7447 listener,
|
|
60
|
+
// so the plugin must join the router too or `execute` queries never reach it.
|
|
61
|
+
function muscatZenohEnv(container) {
|
|
62
|
+
const r = spawnSync("docker", ["inspect", "-f", "{{range .Config.Env}}{{println .}}{{end}}", container], { encoding: "utf8" });
|
|
63
|
+
return /^IS_DEV=(true|1)$/im.test(r.stdout || "")
|
|
64
|
+
? ["-e", "ZENOH_PEER_CONNECT_ENDPOINTS=tcp/zenoh-router:7447"]
|
|
65
|
+
: [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function backendNetwork(portRanges, robotGateway) {
|
|
69
|
+
const muscat = process.env.MUSCAT_PK_MUSCAT_CONTAINER;
|
|
70
|
+
if (muscat) return { run: ["--network", `container:${muscat}`], env: muscatZenohEnv(muscat), shared: true };
|
|
71
|
+
// The robot runs Muscat: its gateway, own netns (host network on Linux), declared ports published.
|
|
72
|
+
if (robotGateway) {
|
|
73
|
+
const run = usesHostNetwork() ? ["--network", "host"] : publishPortFlags(portRanges);
|
|
74
|
+
return { run, env: ["-e", `RB_PLUGIN_GATEWAY_URL=${robotGateway}`], shared: usesHostNetwork() };
|
|
75
|
+
}
|
|
76
|
+
if (usesHostNetwork()) return { run: ["--network", "host"], env: [], shared: true };
|
|
77
|
+
return {
|
|
78
|
+
// host.docker.internal is automatic on Docker Desktop; this makes it resolve on Linux too.
|
|
79
|
+
run: [...publishPortFlags(portRanges), "--add-host", "host.docker.internal:host-gateway"],
|
|
80
|
+
env: ["-e", "RB_PLUGIN_GATEWAY_URL=http://host.docker.internal:10000"],
|
|
81
|
+
shared: false,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// rb.rosPeers in dev: answer muscat/plugin/<id>/ros_peers/{get,set} on the gateway the way
|
|
86
|
+
// Muscat (common) does on the robot — keep the list in .muscat-dev/ and restart the node
|
|
87
|
+
// with ROS_STATIC_PEERS.
|
|
88
|
+
const ROS_PEERS_FILE = ".muscat-dev/ros-peers.json";
|
|
89
|
+
|
|
90
|
+
function readRosPeers(root) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(readFileSync(resolve(root, ROS_PEERS_FILE), "utf8"));
|
|
93
|
+
} catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function serveRosPeers(root, pluginId, restart) {
|
|
99
|
+
if (!pluginId) return;
|
|
100
|
+
const { RbWs } = await import("@rainbow-robotics/plugin-ui/ws");
|
|
101
|
+
const ws = await Promise.race([
|
|
102
|
+
RbWs.connect("http://127.0.0.1:10000", pluginId).catch(() => null),
|
|
103
|
+
new Promise((r) => setTimeout(() => r(null), 5000)),
|
|
104
|
+
]);
|
|
105
|
+
if (!ws) return log.warn("rb.rosPeers unavailable in dev: plugin gateway not reachable on :10000");
|
|
106
|
+
const base = `/muscat/plugin/${pluginId}/ros_peers`;
|
|
107
|
+
await ws.serve(`${base}/get`, () => ({ peers: readRosPeers(root) }));
|
|
108
|
+
await ws.serve(`${base}/set`, (payload) => {
|
|
109
|
+
const peers = payload?.peers;
|
|
110
|
+
if (!Array.isArray(peers) || !peers.every((p) => typeof p === "string" && isIP(p))) {
|
|
111
|
+
return { ok: false, error: "peers must be a list of IP addresses" };
|
|
112
|
+
}
|
|
113
|
+
const file = resolve(root, ROS_PEERS_FILE);
|
|
114
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
115
|
+
writeFileSync(file, JSON.stringify(peers) + "\n");
|
|
116
|
+
log.step(`ROS_STATIC_PEERS → ${peers.join(";") || "(none)"} — restarting node`);
|
|
117
|
+
restart();
|
|
118
|
+
return { ok: true, peers };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── IDE tooling venv (best-effort, non-blocking) ────────────────────────────
|
|
123
|
+
// `muscat-pk new` already creates this up front (see new.mjs); this just
|
|
124
|
+
// refreshes it on every `dev`/`dev:back` in case requirements.txt changed.
|
|
125
|
+
// Docker remains the actual execution environment — this never affects it.
|
|
126
|
+
// Runs in the background so it never delays `dev:back` starting, and any
|
|
127
|
+
// failure here (no local Python toolchain, offline, ...) is silent.
|
|
128
|
+
function syncIdeVenv(root) {
|
|
129
|
+
setImmediate(() => {
|
|
130
|
+
try {
|
|
131
|
+
ensureIdeVenv(root);
|
|
132
|
+
} catch {
|
|
133
|
+
/* best-effort only */
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── python ────────────────────────────────────────────────────────────────────
|
|
139
|
+
// Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
|
|
140
|
+
// Under `muscat-pk dev`, it shares the local Muscat container's netns, so the SDK
|
|
141
|
+
// reaches the plugin gateway at the default localhost:10000.
|
|
142
|
+
async function runPython(root, portRanges, robotGateway) {
|
|
143
|
+
// Independent of Docker readiness — must still happen if Docker isn't
|
|
144
|
+
// installed/running, since that's exactly when local IDE tooling matters most.
|
|
145
|
+
syncIdeVenv(root);
|
|
146
|
+
pyAssertDockerReady();
|
|
147
|
+
await pyEnsureImage();
|
|
148
|
+
|
|
149
|
+
const manifest = readManifest(root);
|
|
150
|
+
const pluginId = manifest.id ?? "";
|
|
151
|
+
const container = `${PYTHON_DEV_IMAGE}-run`;
|
|
152
|
+
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
153
|
+
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
154
|
+
const zenohAdvertiseHost = process.env.MUSCAT_ZENOH_ADVERTISE_HOST ?? "";
|
|
155
|
+
const zenohPort = process.env.MUSCAT_ZENOH_PORT ?? "";
|
|
156
|
+
// See host_agent.py: a Docker Desktop container can't join the robot's
|
|
157
|
+
// Zenoh mesh, so it speaks a byte protocol to the host-side proxy instead.
|
|
158
|
+
const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT ?? "";
|
|
159
|
+
// Native Linux (not WSL, not Docker Desktop) has real host networking: the
|
|
160
|
+
// container shares the host's network stack, so it reaches the gateway at
|
|
161
|
+
// localhost:10000 and the robot's Zenoh router / reverse-dial path directly
|
|
162
|
+
// — no netns sharing, port publishing, host.docker.internal, or byte-proxy.
|
|
163
|
+
const net = backendNetwork(portRanges, robotGateway);
|
|
164
|
+
// Only the container's OWN reverse-dial path needs to publish a listen port.
|
|
165
|
+
// With a shared netns the port is already reachable; with the byte-proxy
|
|
166
|
+
// the container just connects out — neither needs -p.
|
|
167
|
+
const standaloneZenohPortFlags =
|
|
168
|
+
!net.shared && !proxyEndpoint && zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : [];
|
|
169
|
+
// C++ SDK (rainbow-rb-sdk >= 0.10) can't use the byte shim or the fallback patch: its Zenoh session
|
|
170
|
+
// is native. It joins the robot's mesh itself on this host port (see the entry script); inside,
|
|
171
|
+
// the port + 1 accepts. With or without host_agent (dev vs standalone dev:back).
|
|
172
|
+
const sdkAdvertiseHost = proxyEndpoint ? proxyEndpoint.split(":")[0] : zenohAdvertiseHost;
|
|
173
|
+
const sdkPeerPort = sdkAdvertiseHost && !net.shared && zenohPort ? await getFreePort(Number(zenohPort) + 1) : "";
|
|
174
|
+
const sdkPeerFlags = sdkPeerPort
|
|
175
|
+
? ["--sysctl", "net.ipv4.ip_nonlocal_bind=1", "-p", `${sdkPeerPort}:${Number(sdkPeerPort) + 1}`]
|
|
176
|
+
: [];
|
|
177
|
+
const envFlags = [
|
|
178
|
+
// docker exec below has no -t (no pseudo-TTY), so Python defaults to
|
|
179
|
+
// block-buffered stdout — print() output wouldn't show up until the
|
|
180
|
+
// buffer fills or the process exits. Force unbuffered.
|
|
181
|
+
"-e", "PYTHONUNBUFFERED=1",
|
|
182
|
+
...net.env,
|
|
183
|
+
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
184
|
+
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
185
|
+
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
186
|
+
// Byte-proxy mode: the container has no Zenoh session of its own — the
|
|
187
|
+
// entry script's shim speaks the raw query/put/subscribe protocol to the
|
|
188
|
+
// host_agent process at this address instead.
|
|
189
|
+
...(proxyEndpoint ? ["-e", `MUSCAT_PROXY_ENDPOINT=${proxyEndpoint}`] : []),
|
|
190
|
+
...(sdkPeerPort ? ["-e", `MUSCAT_SDK_PEER_PORT=${sdkPeerPort}`, "-e", `MUSCAT_SDK_ADVERTISE_HOST=${sdkAdvertiseHost}`] : []),
|
|
191
|
+
// Fallback (no proxy): the container reverse-dials the robot itself.
|
|
192
|
+
...(!proxyEndpoint && zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
|
|
193
|
+
...(!proxyEndpoint && zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
|
|
194
|
+
...portEnvFlags(portRanges),
|
|
195
|
+
];
|
|
196
|
+
const docker = (args, opts = {}) =>
|
|
197
|
+
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
198
|
+
|
|
199
|
+
// start a warm, long-lived container with the plugin mounted.
|
|
200
|
+
// 호스트 uid 로 돌린다(dockerUserFlags) — root 로 쓴 build/·install/ 은 사용자가 지우지 못한다.
|
|
201
|
+
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
202
|
+
const up = docker(
|
|
203
|
+
["run", "-d", "--name", container, ...net.run, ...standaloneZenohPortFlags, ...sdkPeerFlags, ...realtimeFlags(), ...dockerUserFlags(),
|
|
204
|
+
"-e", "PIP_ROOT_USER_ACTION=ignore",
|
|
205
|
+
"-v", `${root}:/plugin`, "-w", "/plugin", PYTHON_DEV_IMAGE, "sleep", "infinity"],
|
|
206
|
+
{ stdio: ["ignore", "ignore", "inherit"] }, // stdout is just the container id
|
|
207
|
+
);
|
|
208
|
+
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
209
|
+
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [zenohPort, sdkPeerPort, sdkPeerPort && String(Number(sdkPeerPort) + 1)] });
|
|
210
|
+
|
|
211
|
+
docker(["exec", container, "bash", "-lc",
|
|
212
|
+
"pip install --break-system-packages --ignore-installed -q watchfiles" +
|
|
213
|
+
" && { [ -f requirements.txt ] && pip install --break-system-packages --ignore-installed -q -r requirements.txt || true; }"]);
|
|
214
|
+
|
|
215
|
+
docker(["exec", container, "bash", "-lc", `cat > /tmp/muscat-pk-python-dev-entry.py <<'PY'
|
|
216
|
+
import os
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
import glob
|
|
220
|
+
import importlib.util
|
|
221
|
+
|
|
222
|
+
proxy_endpoint = os.getenv("MUSCAT_PROXY_ENDPOINT")
|
|
223
|
+
# Located without importing: importing rainbow.rb_sdk opens the SDK's Zenoh session.
|
|
224
|
+
_rainbow = importlib.util.find_spec("rainbow")
|
|
225
|
+
cpp_sdk = bool(_rainbow and any(glob.glob(os.path.join(p, "rb_sdk", "rb_sdk_cpp*.so")) for p in _rainbow.submodule_search_locations))
|
|
226
|
+
if cpp_sdk and os.getenv("MUSCAT_SDK_PEER_PORT"):
|
|
227
|
+
# C++ SDK (rainbow-rb-sdk >= 0.10): its Zenoh session is native, so the byte shim below
|
|
228
|
+
# can't reach it, and connect() would register the container's own unreachable IP.
|
|
229
|
+
# Robot peers dial each remote peer at the locators it gossips, so the session must
|
|
230
|
+
# advertise the host's ip:port: listen there (ip_nonlocal_bind; never accepts) plus on
|
|
231
|
+
# port + 1, where Docker forwards the host port. Then register like host_agent does.
|
|
232
|
+
import json
|
|
233
|
+
import threading
|
|
234
|
+
import time
|
|
235
|
+
import urllib.request
|
|
236
|
+
|
|
237
|
+
from rainbow.rb_sdk import rb_sdk_cpp
|
|
238
|
+
|
|
239
|
+
_port = int(os.environ["MUSCAT_SDK_PEER_PORT"])
|
|
240
|
+
_advertise = os.environ["MUSCAT_SDK_ADVERTISE_HOST"]
|
|
241
|
+
os.environ["ZENOH_PEER_LISTEN_ENDPOINTS"] = f"tcp/{_advertise}:{_port},tcp/0.0.0.0:{_port + 1}"
|
|
242
|
+
for _cls in list(vars(rb_sdk_cpp).values()):
|
|
243
|
+
if isinstance(_cls, type) and issubclass(_cls, rb_sdk_cpp.RBBaseSDK) and "connect" in _cls.__dict__:
|
|
244
|
+
_cls.connect = lambda self, *args, **kwargs: None
|
|
245
|
+
|
|
246
|
+
def _peer_http(method, path, body=None):
|
|
247
|
+
headers = {"Content-Type": "application/json"}
|
|
248
|
+
if os.getenv("MUSCAT_ROBOT_TOKEN"):
|
|
249
|
+
headers["Authorization"] = "Bearer " + os.environ["MUSCAT_ROBOT_TOKEN"]
|
|
250
|
+
req = urllib.request.Request(f"http://{os.environ['MUSCAT_ROBOT_HOST']}:8000{path}",
|
|
251
|
+
data=json.dumps(body).encode() if body is not None else None,
|
|
252
|
+
headers=headers, method=method)
|
|
253
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
254
|
+
raw = resp.read()
|
|
255
|
+
return json.loads(raw) if raw else {}
|
|
256
|
+
|
|
257
|
+
def _peer_keepalive():
|
|
258
|
+
advertise = _advertise
|
|
259
|
+
peer_id = None
|
|
260
|
+
while True:
|
|
261
|
+
try:
|
|
262
|
+
if peer_id is None:
|
|
263
|
+
peer_id = _peer_http("POST", "/zenoh/peer", {"ip": advertise, "port": _port, "ttl": 30})["peer_id"]
|
|
264
|
+
print(f"[muscat-pk] C++ SDK zenoh peer {advertise}:{_port} registered with the robot", flush=True)
|
|
265
|
+
elif not _peer_http("POST", f"/zenoh/peer/{peer_id}/heartbeat", {"ttl": 30}).get("ok"):
|
|
266
|
+
peer_id = None
|
|
267
|
+
continue
|
|
268
|
+
except Exception as exc:
|
|
269
|
+
print(f"[muscat-pk] zenoh peer registration failed ({exc}); retrying", flush=True)
|
|
270
|
+
peer_id = None
|
|
271
|
+
time.sleep(15 if peer_id else 3)
|
|
272
|
+
|
|
273
|
+
threading.Thread(target=_peer_keepalive, daemon=True).start()
|
|
274
|
+
elif proxy_endpoint:
|
|
275
|
+
# Byte-proxy mode: this container has no Zenoh session of its own.
|
|
276
|
+
# ZenohClient's C++ core is replaced with a shim that speaks a raw
|
|
277
|
+
# query/put/subscribe byte protocol to host_agent.py instead.
|
|
278
|
+
import json
|
|
279
|
+
import socket
|
|
280
|
+
import struct
|
|
281
|
+
import threading
|
|
282
|
+
|
|
283
|
+
_proxy_host, _proxy_port_s = proxy_endpoint.rsplit(":", 1)
|
|
284
|
+
_proxy_port = int(_proxy_port_s)
|
|
285
|
+
|
|
286
|
+
def _send_frame(conn, lock, obj):
|
|
287
|
+
body = json.dumps(obj).encode()
|
|
288
|
+
with lock:
|
|
289
|
+
conn.sendall(struct.pack(">I", len(body)) + body)
|
|
290
|
+
|
|
291
|
+
def _recv_frame(conn):
|
|
292
|
+
hdr = b""
|
|
293
|
+
while len(hdr) < 4:
|
|
294
|
+
chunk = conn.recv(4 - len(hdr))
|
|
295
|
+
if not chunk:
|
|
296
|
+
return None
|
|
297
|
+
hdr += chunk
|
|
298
|
+
n = struct.unpack(">I", hdr)[0]
|
|
299
|
+
buf = b""
|
|
300
|
+
while len(buf) < n:
|
|
301
|
+
chunk = conn.recv(n - len(buf))
|
|
302
|
+
if not chunk:
|
|
303
|
+
return None
|
|
304
|
+
buf += chunk
|
|
305
|
+
return json.loads(buf.decode())
|
|
306
|
+
|
|
307
|
+
class _ProxyRawClient:
|
|
308
|
+
"""Drop-in replacement for ZenohClient's C++ core, backed by the
|
|
309
|
+
host_agent byte-proxy instead of a local Zenoh session."""
|
|
310
|
+
|
|
311
|
+
sender = "RRS"
|
|
312
|
+
sender_id = ""
|
|
313
|
+
|
|
314
|
+
def __init__(self, host, port):
|
|
315
|
+
self._conn = socket.create_connection((host, port), timeout=10)
|
|
316
|
+
# create_connection() leaves the connect timeout applied to
|
|
317
|
+
# the socket permanently; the background read loop below does
|
|
318
|
+
# a long-lived blocking recv() for async "sample" pushes, so
|
|
319
|
+
# it must not time out on an idle connection.
|
|
320
|
+
self._conn.settimeout(None)
|
|
321
|
+
self._send_lock = threading.Lock()
|
|
322
|
+
self._req_id = 0
|
|
323
|
+
self._req_lock = threading.Lock()
|
|
324
|
+
self._pending = {}
|
|
325
|
+
self._subs = {} # sub_id -> callback
|
|
326
|
+
self._sub_by_keyexpr = {}
|
|
327
|
+
threading.Thread(target=self._read_loop, daemon=True).start()
|
|
328
|
+
|
|
329
|
+
def _next_id(self):
|
|
330
|
+
with self._req_lock:
|
|
331
|
+
self._req_id += 1
|
|
332
|
+
return self._req_id
|
|
333
|
+
|
|
334
|
+
def _read_loop(self):
|
|
335
|
+
while True:
|
|
336
|
+
frame = _recv_frame(self._conn)
|
|
337
|
+
if frame is None:
|
|
338
|
+
return
|
|
339
|
+
ftype = frame.get("type")
|
|
340
|
+
if ftype == "sample":
|
|
341
|
+
cb = self._sub_by_keyexpr.get(frame["keyexpr"])
|
|
342
|
+
if cb is not None:
|
|
343
|
+
try:
|
|
344
|
+
cb(frame["key"], bytes.fromhex(frame["payload_hex"]), frame.get("att", ""))
|
|
345
|
+
except Exception as exc:
|
|
346
|
+
print(f"[muscat-pk] proxy subscriber callback error: {exc}", flush=True)
|
|
347
|
+
elif "id" in frame:
|
|
348
|
+
ev = self._pending.pop(frame["id"], None)
|
|
349
|
+
if ev is not None:
|
|
350
|
+
ev[1] = frame
|
|
351
|
+
ev[0].set()
|
|
352
|
+
|
|
353
|
+
def _request(self, obj, timeout=30):
|
|
354
|
+
req_id = self._next_id()
|
|
355
|
+
obj["id"] = req_id
|
|
356
|
+
ev = threading.Event()
|
|
357
|
+
slot = [ev, None]
|
|
358
|
+
self._pending[req_id] = slot
|
|
359
|
+
_send_frame(self._conn, self._send_lock, obj)
|
|
360
|
+
if not ev.wait(timeout):
|
|
361
|
+
self._pending.pop(req_id, None)
|
|
362
|
+
raise TimeoutError(f"proxy request timed out: {obj.get('type')} {obj.get('keyexpr')}")
|
|
363
|
+
return slot[1]
|
|
364
|
+
|
|
365
|
+
def query(self, keyexpr, payload, timeout_ms, target):
|
|
366
|
+
resp = self._request({
|
|
367
|
+
"type": "query", "keyexpr": keyexpr,
|
|
368
|
+
"payload_hex": payload.hex() if payload else None,
|
|
369
|
+
"timeout_ms": timeout_ms, "target": target,
|
|
370
|
+
}, timeout=(timeout_ms / 1000) + 5)
|
|
371
|
+
if not resp.get("ok"):
|
|
372
|
+
raise RuntimeError(resp.get("error", "proxy query failed"))
|
|
373
|
+
return [
|
|
374
|
+
(r["key"], bytes.fromhex(r["payload_hex"]), r["att"], r["encoding"], r["is_error"])
|
|
375
|
+
for r in resp["replies"]
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
def query_all(self, keyexpr, payload, timeout_ms, target):
|
|
379
|
+
return self.query(keyexpr, payload, timeout_ms, target)
|
|
380
|
+
|
|
381
|
+
def publish(self, topic, payload, att, congestion_control, priority, express):
|
|
382
|
+
resp = self._request({
|
|
383
|
+
"type": "put", "keyexpr": topic,
|
|
384
|
+
"payload_hex": bytes(payload).hex() if payload else None,
|
|
385
|
+
})
|
|
386
|
+
if not resp.get("ok"):
|
|
387
|
+
raise RuntimeError(resp.get("error", "proxy put failed"))
|
|
388
|
+
|
|
389
|
+
def has_subscriber(self, topic):
|
|
390
|
+
return topic in self._sub_by_keyexpr
|
|
391
|
+
|
|
392
|
+
def declare_subscriber(self, topic, callback, opts=None):
|
|
393
|
+
resp = self._request({"type": "subscribe", "keyexpr": topic})
|
|
394
|
+
if not resp.get("ok"):
|
|
395
|
+
raise RuntimeError(resp.get("error", "proxy subscribe failed"))
|
|
396
|
+
sub_id = self._next_id()
|
|
397
|
+
self._subs[sub_id] = topic
|
|
398
|
+
self._sub_by_keyexpr[topic] = callback
|
|
399
|
+
return sub_id
|
|
400
|
+
|
|
401
|
+
def undeclare_subscriber(self, sub_id):
|
|
402
|
+
topic = self._subs.pop(sub_id, None)
|
|
403
|
+
if topic is None:
|
|
404
|
+
return
|
|
405
|
+
self._sub_by_keyexpr.pop(topic, None)
|
|
406
|
+
self._request({"type": "unsubscribe", "keyexpr": topic})
|
|
407
|
+
|
|
408
|
+
def unsubscribe_topic(self, topic):
|
|
409
|
+
for sub_id, t in list(self._subs.items()):
|
|
410
|
+
if t == topic:
|
|
411
|
+
self.undeclare_subscriber(sub_id)
|
|
412
|
+
|
|
413
|
+
def unsubscribe_all(self):
|
|
414
|
+
for sub_id in list(self._subs):
|
|
415
|
+
self.undeclare_subscriber(sub_id)
|
|
416
|
+
|
|
417
|
+
def get_sub_stats(self, sub_id):
|
|
418
|
+
return {}
|
|
419
|
+
|
|
420
|
+
def declare_queryable(self, keyexpr, handler):
|
|
421
|
+
raise NotImplementedError("byte-proxy mode does not support declare_queryable")
|
|
422
|
+
|
|
423
|
+
def undeclare_queryable_by_topic(self, keyexpr):
|
|
424
|
+
pass
|
|
425
|
+
|
|
426
|
+
# ZenohClient is a process-wide singleton whose __init__ eagerly
|
|
427
|
+
# calls connect() on first construction. rainbow.rb_sdk.base
|
|
428
|
+
# transitively imports rainbow.rb_log.log, which constructs one at
|
|
429
|
+
# module level — so this patch must land before RBBaseSDK is
|
|
430
|
+
# imported, or that eager construction wins with a real session.
|
|
431
|
+
from rainbow.rb_zenoh.client import ZenohClient
|
|
432
|
+
|
|
433
|
+
def _proxy_connect(self, *, config_pairs=None, sender=None, max_attempts=8, initial_delay_s=0.5, max_delay_s=3.0):
|
|
434
|
+
if self._client is not None:
|
|
435
|
+
return
|
|
436
|
+
if sender is not None:
|
|
437
|
+
self._sender = sender
|
|
438
|
+
print(f"[muscat-pk] zenoh via host byte-proxy → {proxy_endpoint} (no local Zenoh session)", flush=True)
|
|
439
|
+
self._client = _ProxyRawClient(_proxy_host, _proxy_port)
|
|
440
|
+
|
|
441
|
+
ZenohClient.connect = _proxy_connect
|
|
442
|
+
|
|
443
|
+
from rainbow.rb_sdk.base import RBBaseSDK
|
|
444
|
+
|
|
445
|
+
async def _sdk_connect_via_proxy(self, *args, **kwargs):
|
|
446
|
+
# No HTTP registration with common — host_agent already handles
|
|
447
|
+
# that. Just make sure the (proxy-backed) shared ZenohClient exists.
|
|
448
|
+
self._ensure_zenoh_client_alive()
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
async def _sdk_disconnect_via_proxy(self, *args, **kwargs):
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
RBBaseSDK.connect = _sdk_connect_via_proxy
|
|
455
|
+
RBBaseSDK.disconnect = _sdk_disconnect_via_proxy
|
|
456
|
+
else:
|
|
457
|
+
# Fallback: no host byte-proxy — the container reverse-dials the robot itself.
|
|
458
|
+
advertise_host = os.getenv("MUSCAT_ZENOH_ADVERTISE_HOST")
|
|
459
|
+
zenoh_port = os.getenv("MUSCAT_ZENOH_PORT")
|
|
460
|
+
from rainbow.rb_sdk.base import RBBaseSDK
|
|
461
|
+
if advertise_host:
|
|
462
|
+
RBBaseSDK._detect_local_ip = staticmethod(lambda host: advertise_host)
|
|
463
|
+
if zenoh_port:
|
|
464
|
+
_orig_connect = RBBaseSDK.connect
|
|
465
|
+
|
|
466
|
+
async def _connect_with_dev_port(self, *args, **kwargs):
|
|
467
|
+
kwargs.setdefault("zenoh_port", int(zenoh_port))
|
|
468
|
+
return await _orig_connect(self, *args, **kwargs)
|
|
469
|
+
|
|
470
|
+
RBBaseSDK.connect = _connect_with_dev_port
|
|
471
|
+
except Exception as exc:
|
|
472
|
+
print(f"[muscat-pk] SDK dev network patch skipped: {exc}", flush=True)
|
|
473
|
+
|
|
474
|
+
import runpy
|
|
475
|
+
runpy.run_path("/plugin/py/main.py", run_name="__main__")
|
|
476
|
+
PY`]);
|
|
477
|
+
|
|
478
|
+
if (usesHostNetwork()) log.step("network: host (native Linux — direct robot/gateway access)");
|
|
479
|
+
if (proxyEndpoint) log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
|
|
480
|
+
else if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
|
|
481
|
+
const child = spawn(
|
|
482
|
+
"docker",
|
|
483
|
+
["exec", ...envFlags, container, "bash", "-lc",
|
|
484
|
+
"python3 -m watchfiles --filter python --target-type command 'python3 /tmp/muscat-pk-python-dev-entry.py' py"],
|
|
485
|
+
{ cwd: root, stdio: ["inherit", "inherit", "pipe"] },
|
|
486
|
+
);
|
|
487
|
+
// watchfiles prints a KeyboardInterrupt traceback on SIGTERM — suppress it
|
|
488
|
+
child.stderr.on("data", (buf) => {
|
|
489
|
+
const text = buf.toString();
|
|
490
|
+
if (!text.includes("KeyboardInterrupt") && !text.includes("raise_keyboard_interrupt")) {
|
|
491
|
+
process.stderr.write(buf);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
let cleaned = false;
|
|
496
|
+
const cleanupResources = () => {
|
|
497
|
+
if (cleaned) return;
|
|
498
|
+
cleaned = true;
|
|
499
|
+
stopPortMonitor();
|
|
500
|
+
if (!child.killed && child.exitCode === null) child.kill();
|
|
501
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
502
|
+
};
|
|
503
|
+
const cleanup = () => {
|
|
504
|
+
cleanupResources();
|
|
505
|
+
exitAfterCleanup(0);
|
|
506
|
+
};
|
|
507
|
+
child.on("exit", (code) => {
|
|
508
|
+
cleanupResources();
|
|
509
|
+
exitAfterCleanup(code ?? 0);
|
|
510
|
+
});
|
|
511
|
+
process.once("SIGINT", cleanup);
|
|
512
|
+
process.once("SIGTERM", cleanup);
|
|
513
|
+
process.once("SIGHUP", cleanup);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── ros2 ───────────────────────────────────────────────────────────────────────
|
|
517
|
+
async function runRos2(root, portRanges, robotGateway) {
|
|
518
|
+
const manifest = readManifest(root);
|
|
519
|
+
const isRclcpp = manifest.runtime?.rosClient === "rclcpp";
|
|
520
|
+
// Independent of Docker readiness — must still happen if Docker isn't
|
|
521
|
+
// installed/running, since that's exactly when local IDE tooling matters most.
|
|
522
|
+
if (!isRclcpp) syncIdeVenv(root); // rclpy is plain Python — rclcpp is C++, no venv needed
|
|
523
|
+
|
|
524
|
+
assertDockerReady();
|
|
525
|
+
const pkg = ros2PackageName(root);
|
|
526
|
+
if (!pkg) throw new Error("no ROS2 package found under ros2_ws/src.");
|
|
527
|
+
|
|
528
|
+
await ensureImage();
|
|
529
|
+
|
|
530
|
+
const pluginId = manifest.id ?? "";
|
|
531
|
+
const container = `${ROS2_DEV_IMAGE}-run`;
|
|
532
|
+
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
533
|
+
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
534
|
+
const net = backendNetwork(portRanges, robotGateway);
|
|
535
|
+
const envFlags = [
|
|
536
|
+
// Same rationale as runPython(): no -t on docker exec below means
|
|
537
|
+
// rclpy's Python process stdout is block-buffered, not line-buffered.
|
|
538
|
+
"-e", "PYTHONUNBUFFERED=1",
|
|
539
|
+
// Same as the robot: ROS discovery stays on this host (allow a PC with ROS_STATIC_PEERS).
|
|
540
|
+
"-e", "ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST",
|
|
541
|
+
...net.env,
|
|
542
|
+
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
543
|
+
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
544
|
+
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
545
|
+
...portEnvFlags(portRanges),
|
|
546
|
+
];
|
|
547
|
+
const docker = (args, opts = {}) =>
|
|
548
|
+
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
549
|
+
|
|
550
|
+
// start a warm, long-lived container with the workspace mounted
|
|
551
|
+
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
552
|
+
const up = docker(
|
|
553
|
+
["run", "-d", "--name", container, ...net.run, ...realtimeFlags(), ...dockerUserFlags(), "-e", "PIP_ROOT_USER_ACTION=ignore", "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
|
|
554
|
+
{ stdio: ["ignore", "ignore", "inherit"] }, // stdout is just the container id
|
|
555
|
+
);
|
|
556
|
+
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
557
|
+
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
|
|
558
|
+
|
|
559
|
+
// one-time deps + symlink build (edits to .py reflect without rebuilding)
|
|
560
|
+
docker(["exec", container, "bash", "-lc",
|
|
561
|
+
"pip install -r requirements.txt --break-system-packages --ignore-installed -q || true; cd ros2_ws && colcon build --symlink-install"]);
|
|
562
|
+
|
|
563
|
+
let child = null;
|
|
564
|
+
const startNode = () => {
|
|
565
|
+
if (child) child.kill();
|
|
566
|
+
// killing the `docker exec` client doesn't stop the node inside the container
|
|
567
|
+
spawnSync("docker", ["exec", container, "pkill", "-f", `${pkg}_node|${manifest.runtime?.launch || pkg}`], { stdio: "ignore" });
|
|
568
|
+
const peers = readRosPeers(root);
|
|
569
|
+
// Same command the robot runs: rclpy execs the console script, rclcpp uses `ros2 run`;
|
|
570
|
+
// relative topics land under the plugin's namespace (rosNamespace).
|
|
571
|
+
const launch = manifest.runtime?.launch || `${pkg}_node`;
|
|
572
|
+
const nsArgs = PLUGIN_ID_RE.test(pluginId) ? ` --ros-args -r __ns:=${rosNamespace(pluginId)}` : "";
|
|
573
|
+
const run = (isRclcpp ? `ros2 run ${pkg} ${launch}` : `exec ${launch}`) + nsArgs;
|
|
574
|
+
child = spawn(
|
|
575
|
+
"docker",
|
|
576
|
+
["exec", ...envFlags, ...(peers.length ? ["-e", `ROS_STATIC_PEERS=${peers.join(";")}`] : []), container, "bash", "-lc",
|
|
577
|
+
`cd ros2_ws && source install/setup.bash && ${run}`],
|
|
578
|
+
{ cwd: root, stdio: "inherit" },
|
|
579
|
+
);
|
|
580
|
+
};
|
|
581
|
+
startNode();
|
|
582
|
+
serveRosPeers(root, pluginId, startNode);
|
|
583
|
+
|
|
584
|
+
// restart node on source change. rclpy reflects .py edits live (symlink-install);
|
|
585
|
+
// rclcpp (C++) and package-metadata changes require a colcon rebuild.
|
|
586
|
+
let timer = null;
|
|
587
|
+
const onChange = (_event, filename) => {
|
|
588
|
+
clearTimeout(timer);
|
|
589
|
+
timer = setTimeout(() => {
|
|
590
|
+
const structural = filename && /(setup\.py|package\.xml|CMakeLists\.txt)$/.test(String(filename));
|
|
591
|
+
if (structural || isRclcpp) {
|
|
592
|
+
log.info(structural ? "package metadata changed — colcon rebuild…" : "source changed — colcon rebuild…");
|
|
593
|
+
docker(["exec", container, "bash", "-lc", "cd ros2_ws && colcon build --symlink-install"]);
|
|
594
|
+
} else {
|
|
595
|
+
log.info("source changed — restarting node…");
|
|
596
|
+
}
|
|
597
|
+
startNode();
|
|
598
|
+
}, 300);
|
|
599
|
+
};
|
|
600
|
+
for (const target of ["ros2_ws/src", "main.py"]) {
|
|
601
|
+
const full = resolve(root, target);
|
|
602
|
+
if (existsSync(full)) watch(full, { recursive: true }, onChange);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const cleanup = () => {
|
|
606
|
+
stopPortMonitor();
|
|
607
|
+
if (child) child.kill();
|
|
608
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
609
|
+
exitAfterCleanup(0);
|
|
610
|
+
};
|
|
611
|
+
process.on("SIGINT", cleanup);
|
|
612
|
+
process.on("SIGTERM", cleanup);
|
|
613
|
+
process.on("SIGHUP", cleanup);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ── cpp ──────────────────────────────────────────────────────────────────────
|
|
617
|
+
async function runCpp(root, portRanges, robotGateway) {
|
|
618
|
+
cppAssertDockerReady();
|
|
619
|
+
const manifest = readManifest(root);
|
|
620
|
+
const entry = manifest.runtime?.entry;
|
|
621
|
+
if (!entry) throw new Error("runtime.entry (the built executable name) is required for cpp.");
|
|
622
|
+
|
|
623
|
+
await cppEnsureImage();
|
|
624
|
+
|
|
625
|
+
const pluginId = manifest.id ?? "";
|
|
626
|
+
const container = `${CPP_DEV_IMAGE}-run`;
|
|
627
|
+
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
628
|
+
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
629
|
+
const net = backendNetwork(portRanges, robotGateway);
|
|
630
|
+
const envFlags = [
|
|
631
|
+
...net.env,
|
|
632
|
+
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
633
|
+
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
634
|
+
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
635
|
+
...portEnvFlags(portRanges),
|
|
636
|
+
];
|
|
637
|
+
const docker = (args, opts = {}) =>
|
|
638
|
+
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
639
|
+
|
|
640
|
+
// start a warm, long-lived container with the plugin mounted
|
|
641
|
+
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
642
|
+
const up = docker(
|
|
643
|
+
["run", "-d", "--name", container, ...net.run, ...realtimeFlags(), ...dockerUserFlags(), "-v", `${root}:/plugin`, "-w", "/plugin", CPP_DEV_IMAGE, "sleep", "infinity"],
|
|
644
|
+
{ stdio: ["ignore", "ignore", "inherit"] }, // stdout is just the container id
|
|
645
|
+
);
|
|
646
|
+
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
647
|
+
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
|
|
648
|
+
|
|
649
|
+
const build = () => {
|
|
650
|
+
return docker(["exec", container, "bash", "-lc",
|
|
651
|
+
"cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Debug && cmake --build cpp/build -j"]);
|
|
652
|
+
};
|
|
653
|
+
build();
|
|
654
|
+
|
|
655
|
+
let child = null;
|
|
656
|
+
const startBin = () => {
|
|
657
|
+
if (child) child.kill();
|
|
658
|
+
child = spawn(
|
|
659
|
+
"docker",
|
|
660
|
+
["exec", ...envFlags, container, "bash", "-lc", `./cpp/build/${entry}`],
|
|
661
|
+
{ cwd: root, stdio: "inherit" },
|
|
662
|
+
);
|
|
663
|
+
};
|
|
664
|
+
startBin();
|
|
665
|
+
|
|
666
|
+
// rebuild + restart on C++ source changes; ignore the build/ output dir.
|
|
667
|
+
let timer = null;
|
|
668
|
+
const onChange = (_event, filename) => {
|
|
669
|
+
if (filename && String(filename).startsWith("build")) return;
|
|
670
|
+
clearTimeout(timer);
|
|
671
|
+
timer = setTimeout(() => {
|
|
672
|
+
log.info("source changed — rebuilding…");
|
|
673
|
+
const r = build();
|
|
674
|
+
if (r.status === 0) startBin();
|
|
675
|
+
else log.warn("build failed — keeping previous binary");
|
|
676
|
+
}, 300);
|
|
677
|
+
};
|
|
678
|
+
const cppDir = resolve(root, "cpp");
|
|
679
|
+
if (existsSync(cppDir)) watch(cppDir, { recursive: true }, onChange);
|
|
680
|
+
|
|
681
|
+
const cleanup = () => {
|
|
682
|
+
stopPortMonitor();
|
|
683
|
+
if (child) child.kill();
|
|
684
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
685
|
+
exitAfterCleanup(0);
|
|
686
|
+
};
|
|
687
|
+
process.on("SIGINT", cleanup);
|
|
688
|
+
process.on("SIGTERM", cleanup);
|
|
689
|
+
process.on("SIGHUP", cleanup);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function warnManifest(root) {
|
|
693
|
+
const manifest = readManifest(root);
|
|
694
|
+
if (!manifest || Object.keys(manifest).length === 0) return;
|
|
695
|
+
const errors = validateManifest(manifest);
|
|
696
|
+
for (const e of errors) log.warn(`manifest.json: ${e}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export async function runDevBack(args = []) {
|
|
700
|
+
const root = findPluginRoot();
|
|
701
|
+
assertManifestVariables(root);
|
|
702
|
+
warnManifest(root);
|
|
703
|
+
const portRanges = assertManifestPorts(root);
|
|
704
|
+
ensureDocker();
|
|
705
|
+
const { robotIp, robotToken } = await promptRobotConnection(args);
|
|
706
|
+
if (robotIp) process.env.MUSCAT_ROBOT_HOST = robotIp;
|
|
707
|
+
if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
|
|
708
|
+
// Standalone `dev:back` against a remote robot needs the Zenoh reverse-path
|
|
709
|
+
// advertise host/port too (idempotent — `muscat-pk dev` already set these).
|
|
710
|
+
// Byte-proxy mode (a host-side host_agent set MUSCAT_PROXY_ENDPOINT) makes
|
|
711
|
+
// the container speak the proxy protocol instead, so it needs no reverse-dial.
|
|
712
|
+
if (!process.env.MUSCAT_PROXY_ENDPOINT) await ensureRemoteZenohEnv(robotIp, { log });
|
|
713
|
+
// Standalone against a local Muscat (muscat-dev, …): join its netns like `dev` does,
|
|
714
|
+
// otherwise zenoh reaches nothing and `execute` never arrives.
|
|
715
|
+
if (!process.env.MUSCAT_PK_MUSCAT_CONTAINER && !isRemoteRobot(robotIp)) {
|
|
716
|
+
const muscat = localMuscatContainer();
|
|
717
|
+
if (muscat) {
|
|
718
|
+
process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Stopped while still setting up (image build, pip install, first cmake build): the runtime's own
|
|
722
|
+
// cleanup is not registered yet, so remove the container here. Once it is, it handles the signal.
|
|
723
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
724
|
+
process.once(signal, () => {
|
|
725
|
+
if (process.listenerCount(signal)) return;
|
|
726
|
+
stopDevBackContainers();
|
|
727
|
+
process.exit(130);
|
|
728
|
+
});
|
|
729
|
+
const robotGateway = (await robotHasMuscat(robotIp)) ? `http://${robotIp}:${GATEWAY_PORT}` : "";
|
|
730
|
+
const type = manifestRuntime(root);
|
|
731
|
+
if (type === "ros2") return await runRos2(root, portRanges, robotGateway);
|
|
732
|
+
if (type === "cpp") return await runCpp(root, portRanges, robotGateway);
|
|
733
|
+
return await runPython(root, portRanges, robotGateway);
|
|
734
|
+
}
|