@rainbow-robotics/muscat-plugin-kit 1.3.6-dev.6 → 1.3.6-dev.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/README.md +3 -8
- package/cli/bin/muscat-pk.mjs +0 -5
- package/cli/bundle-templates.mjs +1 -6
- package/cli/docker/Dockerfile.cpp +6 -28
- package/cli/docker/Dockerfile.dev +0 -32
- package/cli/docker/Dockerfile.python +0 -19
- package/cli/src/bridge.mjs +12 -45
- package/cli/src/build.mjs +79 -48
- package/cli/src/cpp.mjs +5 -11
- package/cli/src/dev.mjs +2 -7
- package/cli/src/devBack.mjs +40 -97
- package/cli/src/host_agent.py +83 -143
- package/cli/src/manifest.mjs +0 -11
- package/cli/src/mcp.mjs +54 -398
- package/cli/src/new.mjs +3 -13
- package/cli/src/ports.mjs +7 -21
- package/cli/src/python.mjs +20 -16
- package/cli/src/ros2.mjs +8 -16
- package/cli/src/sdk.mjs +21 -23
- package/cli/src/templates-embedded.mjs +4 -0
- package/cli/src/type.mjs +10 -44
- package/cli/src/uninstall.mjs +20 -3
- package/cli/src/update.mjs +8 -14
- package/cli/src/util.mjs +9 -70
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +1 -13
- package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +1 -98
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-C5ttlJWI.js → Color-YHDXOIA2-Du8dJ-nM.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CCfiEyzU.js → Docs--Fm8069p.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-B3AV-YvP.js → Docs-Cb72AJnj.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BN206lKl.js → Docs-LdjYwuG4.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BqH2prGO.js → Docs-Sy5RDbtH.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DhncK7Aw.js → Docs-guGflRzZ.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CGN1_Ua8.js → Docs-pwGqGTV_.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-D-w8Kq64.js → DocsRenderer-CFRXHY34-YGBLQ4BD.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-oCcYtLwq.js → NxAvatar.stories-C1d4n8cq.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-C5jUScIj.js → NxButton.stories-gCBoZdMq.js} +1 -1
- package/cli/storybook-dist/assets/{NxIcons.stories-DeqrVzVs.js → NxIcons.stories-Dg42riT5.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-gjsEulIN.js → NxNumberInput.stories-DB-vLM2J.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-BK6uSWIa.js → NxPagination.stories-CS8iMIak.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BDD5CvSZ.js → NxPointerHoldButton.stories-D66BopeS.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-C_nW26_x.js → NxRangeInput.stories-CZxddsJ2.js} +3 -3
- package/cli/storybook-dist/assets/{NxSelectBox.stories-DqLfKU9H.js → NxSelectBox.stories-CmezGGrm.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CQjHA8yv.js → NxSlideRange.stories-CGAFMITi.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-BMHCiPoL.js → NxSwitch.stories-B3QInkXX.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-k7r1WVQh.js → NxTextArea.stories-BgJcgZNe.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-Cqfe1I32.js → NxTextInput.stories-CwMQwI9L.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-CJ3Ttuja.js → RBAccordion.stories-BuBVNzJT.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-BThv7tM0.js → RBAccordionGroup.stories-Crdb6kFF.js} +1 -1
- package/cli/storybook-dist/assets/{RBAlert.stories-CSMP8Vun.js → RBAlert.stories-CqXgUvA2.js} +1 -1
- package/cli/storybook-dist/assets/{RBBadge.stories-cBMA2R6F.js → RBBadge.stories-BCw_ksML.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-BKVOskqs.js → RBBattery.stories-0id4XYoc.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-BTOfK0HC.js → RBCard.stories-CgpG-7Lk.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckBox.stories-DyE912wG.js → RBCheckBox.stories-BG02BoGX.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-0sLFU0kM.js → RBCheckIconButton.stories-vrjD4GQ4.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-c8VecClE.js → RBCheckLabelButton.stories-POba8lU-.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipTab.stories-CQ9Uav5_.js → RBChipTab.stories-XWJbFrHF.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-Ap4NlhAZ.js → RBChipsBoxTab.stories-BdU0Dzdp.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-BPPXjif2.js → RBColorIcons.stories-C_0dCNXG.js} +1 -1
- package/cli/storybook-dist/assets/{RBCommand.stories-PDpYOKlm.js → RBCommand.stories-Dvrko5No.js} +1 -1
- package/cli/storybook-dist/assets/{RBConfirm.stories-BEWvMAYP.js → RBConfirm.stories-CkTn0sHa.js} +1 -1
- package/cli/storybook-dist/assets/{RBDateInput.stories-DLdyNnVF.js → RBDateInput.stories-BH3D08cs.js} +1 -1
- package/cli/storybook-dist/assets/{RBDialog.stories-HsawMNOb.js → RBDialog.stories-C2nNX9fl.js} +1 -1
- package/cli/storybook-dist/assets/{RBDivider.stories-dlWwOxik.js → RBDivider.stories-D-GrJ0QZ.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-C87iqGk0.js → RBDrawer.stories-XLOnLOnh.js} +1 -1
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-B8EQhA0z.js → RBDropdownMenu.stories-BjBSeoV2.js} +1 -1
- package/cli/storybook-dist/assets/{RBEmptyView.stories-SzpIhNDw.js → RBEmptyView.stories-C6U2zKNE.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-BQ0nM1gb.js → RBIconButton.stories-MCJq5Hta.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-C19g5M5z.js → RBIcons.stories-DxOCiCe0.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-DbgI4-1P.js → RBInlineLabelButton.stories-BVZi1aEs.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-smF-vpGL.js → RBJoyStick.stories-BDThbEek.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-s53klwDv.js → RBLabelButton.stories-CaPEKUS6.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-BnzkoR51.js → RBLight.stories-CihQzyEL.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-CW014klu.js → RBMenubar.stories-J852XVFh.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-qa4tH_4Y.js → RBMultiRangeInput.stories-DXyX0cOZ.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-YoQNUpQH.js → RBPagination.stories-C9_49ymO.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-Bhpi0OaH.js → RBPopover.stories-DxLE912-.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-iQM5iAka.js → RBPrompt.stories-DlsfGqWb.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-BGyq4rzx.js → RBRadioBox.stories-hb5cNWW1.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-DF6BcUcJ.js → RBRadioButton.stories-CSuOgeN-.js} +1 -1
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-KRg2glX8.js +15 -0
- package/cli/storybook-dist/assets/{RBResizable.stories-DSVHvwjp.js → RBResizable.stories-BetjF9vG.js} +1 -1
- package/cli/storybook-dist/assets/RBSelectBox.stories-ILLQQ8M8.js +6 -0
- package/cli/storybook-dist/assets/{RBSheet.stories-CAK9hb-p.js → RBSheet.stories-D3QbcMKq.js} +1 -1
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DUymnH-9.js → RBSideNumberInput.stories-DcTh0IjM.js} +1 -1
- package/cli/storybook-dist/assets/{RBSlider.stories-BwlDh9--.js → RBSlider.stories-B59We7ei.js} +1 -1
- package/cli/storybook-dist/assets/{RBSpinner.stories-VOlkuP6t.js → RBSpinner.stories-9u3_ETbi.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-BiW2dQnT.js → RBSwitch.stories-DrVm1WGX.js} +1 -1
- package/cli/storybook-dist/assets/{RBTab.stories-DFKVsy0C.js → RBTab.stories-D8Dm7cRW.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-BWG0ZSYI.js → RBTable.stories-dRdZ8g1j.js} +1 -1
- package/cli/storybook-dist/assets/{RBTableRow.stories-JVTD83Mh.js → RBTableRow.stories-rZeoGwHZ.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-D-aN385u.js → RBTabs.stories-STRV2lgO.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextArea.stories-Dx5xkmZS.js → RBTextArea.stories-CfgUbrbJ.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextField.stories-DmVciExd.js → RBTextField.stories-B5t8-Y97.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextInput.stories-CCf0VGEv.js → RBTextInput.stories-Bf1edN6l.js} +1 -1
- package/cli/storybook-dist/assets/{RBToast.stories-DT0DGn05.js → RBToast.stories-Cnm8JwQ6.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-Ce_q3EtP.js → RBToggle.stories-B0OCo6zd.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-CN40YHsR.js → RBToggleGroup.stories-GdYO2Fxg.js} +1 -1
- package/cli/storybook-dist/assets/{RBTooltip.stories-L7btDEpA.js → RBTooltip.stories-DXJTCScO.js} +1 -1
- package/cli/storybook-dist/assets/SideNumberInput-CrF-a1Nr.js +1114 -0
- package/cli/storybook-dist/assets/browserUtils-CxJ5gea3.js +1 -0
- package/cli/storybook-dist/assets/{entry-preview-D_nGAXUk.js → entry-preview-DSRwIaVJ.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-23VS_ZeI.js → iframe-Cj53Wn3d.js} +2 -2
- package/cli/storybook-dist/assets/{index-ClAc6FBW.js → index-C1OzuK6-.js} +1 -1
- package/cli/storybook-dist/assets/{index-Ckae_HWe.js → index-CkHtLEe8.js} +3 -3
- package/cli/storybook-dist/assets/preview-CEfMaO1t.css +1 -0
- package/cli/storybook-dist/assets/{preview-BVJwlGUF.js → preview-CILRoSnB.js} +1 -1
- package/cli/storybook-dist/assets/{preview-CD0LINug.js → preview-CgOtgtCD.js} +2 -2
- package/cli/storybook-dist/assets/preview-D_2um8Sd.js +3 -0
- package/cli/storybook-dist/assets/react-resizable-panels.browser-CtORL3tG.js +102 -0
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/index.html +1 -1
- package/cli/storybook-dist/index.json +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/README.md +7 -211
- package/cli/templates/common/py/main.py +2 -29
- package/cli/templates/common/requirements.txt +3 -3
- package/cli/templates/cpp/cpp/CMakeLists.txt +7 -6
- package/cli/templates/cpp/cpp/main.cpp +10 -57
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +0 -4
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +4 -5
- package/package.json +3 -3
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-COOQ_HNF.js +0 -15
- package/cli/storybook-dist/assets/RBSelectBox.stories-BookV8Sm.js +0 -74
- package/cli/storybook-dist/assets/SideNumberInput-B5_xLK8z.js +0 -1109
- package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +0 -1
- package/cli/storybook-dist/assets/preview-P8yWWsME.js +0 -3
- package/cli/storybook-dist/assets/preview-zuAvMksL.css +0 -1
- package/cli/storybook-dist/assets/react-resizable-panels.browser-CYcsrUgg.js +0 -120
- package/cli/templates/readme-cpp/README.md +0 -993
package/cli/src/devBack.mjs
CHANGED
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
// ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
|
|
9
9
|
// on source changes (rclcpp rebuilds on change).
|
|
10
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
-
import { existsSync,
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { dockerUserFlags, ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
|
|
11
|
+
import { existsSync, watch } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
|
|
15
14
|
import { ensureRemoteZenohEnv, usesHostNetwork } from "./zenohDev.mjs";
|
|
16
|
-
import {
|
|
15
|
+
import { assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, validateManifest } from "./manifest.mjs";
|
|
17
16
|
import {
|
|
18
17
|
formatPortRanges,
|
|
19
18
|
portEnvFlags,
|
|
@@ -53,67 +52,6 @@ export function stopDevBackContainers() {
|
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
// Where the backend's 127.0.0.1 points. A local Muscat container (muscat-dev): join its
|
|
57
|
-
// netns so the gateway and MediaMTX resolve like on the robot. Under `dev`: the bridge's
|
|
58
|
-
// netns. Standalone dev:back: its own netns, gateway via the host.
|
|
59
|
-
function backendNetwork(portRanges) {
|
|
60
|
-
const muscat = process.env.MUSCAT_PK_MUSCAT_CONTAINER;
|
|
61
|
-
if (muscat) return { run: ["--network", `container:${muscat}`], env: [], shared: true };
|
|
62
|
-
if (usesHostNetwork()) return { run: ["--network", "host"], env: [], shared: true };
|
|
63
|
-
const bridge = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
|
|
64
|
-
if (bridge) return { run: ["--network", `container:${bridge}`], env: [], shared: true };
|
|
65
|
-
return {
|
|
66
|
-
// host.docker.internal is automatic on Docker Desktop; this makes it resolve on Linux too.
|
|
67
|
-
run: [...publishPortFlags(portRanges), "--add-host", "host.docker.internal:host-gateway"],
|
|
68
|
-
env: ["-e", "RB_PLUGIN_GATEWAY_URL=http://host.docker.internal:10000"],
|
|
69
|
-
shared: false,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// rb.rosPeers in dev: answer muscat/plugin/<id>/ros_peers/{get,set} on the gateway the way
|
|
74
|
-
// Muscat (common) does on the robot — keep the list in .muscat-dev/ and restart the node
|
|
75
|
-
// with ROS_STATIC_PEERS.
|
|
76
|
-
const ROS_PEERS_FILE = ".muscat-dev/ros-peers.json";
|
|
77
|
-
|
|
78
|
-
function readRosPeers(root) {
|
|
79
|
-
try {
|
|
80
|
-
return JSON.parse(readFileSync(resolve(root, ROS_PEERS_FILE), "utf8"));
|
|
81
|
-
} catch {
|
|
82
|
-
return [];
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function serveRosPeers(root, pluginId, restart) {
|
|
87
|
-
if (!pluginId) return;
|
|
88
|
-
const { RbWs } = await import("@rainbow-robotics/plugin-ui/ws");
|
|
89
|
-
const ws = await Promise.race([
|
|
90
|
-
RbWs.connect("http://127.0.0.1:10000", pluginId).catch(() => null),
|
|
91
|
-
new Promise((r) => setTimeout(() => r(null), 5000)),
|
|
92
|
-
]);
|
|
93
|
-
if (!ws) return log.warn("rb.rosPeers unavailable in dev: plugin gateway not reachable on :10000");
|
|
94
|
-
const base = `/muscat/plugin/${pluginId}/ros_peers`;
|
|
95
|
-
await ws.serve(`${base}/get`, () => ({ peers: readRosPeers(root) }));
|
|
96
|
-
await ws.serve(`${base}/set`, (payload) => {
|
|
97
|
-
const peers = payload?.peers;
|
|
98
|
-
if (!Array.isArray(peers) || !peers.every((p) => typeof p === "string" && isIP(p))) {
|
|
99
|
-
return { ok: false, error: "peers must be a list of IP addresses" };
|
|
100
|
-
}
|
|
101
|
-
const file = resolve(root, ROS_PEERS_FILE);
|
|
102
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
103
|
-
writeFileSync(file, JSON.stringify(peers) + "\n");
|
|
104
|
-
log.step(`ROS_STATIC_PEERS → ${peers.join(";") || "(none)"} — restarting node`);
|
|
105
|
-
restart();
|
|
106
|
-
return { ok: true, peers };
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Our own MediaMTX, only when `dev` found its ports free (a local Muscat brings its own).
|
|
111
|
-
function startMediaServer(container) {
|
|
112
|
-
if (process.env.MUSCAT_PK_MEDIA !== "1") return;
|
|
113
|
-
spawnSync("docker", ["exec", "-d", container, "mediamtx", "/etc/mediamtx/mediamtx.yml"], { stdio: "ignore" });
|
|
114
|
-
log.step("MediaMTX: publish rtsp://127.0.0.1:8554/<path> · play http://localhost:8889/<path>");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
55
|
// ── IDE tooling venv (best-effort, non-blocking) ────────────────────────────
|
|
118
56
|
// `muscat-pk new` already creates this up front (see new.mjs); this just
|
|
119
57
|
// refreshes it on every `dev`/`dev:back` in case requirements.txt changed.
|
|
@@ -157,18 +95,39 @@ async function runPython(root, portRanges) {
|
|
|
157
95
|
// container shares the host's network stack, so it reaches the bridge at
|
|
158
96
|
// localhost:10000 and the robot's Zenoh router / reverse-dial path directly
|
|
159
97
|
// — no netns sharing, port publishing, host.docker.internal, or byte-proxy.
|
|
160
|
-
const
|
|
98
|
+
const useHostNet = usesHostNetwork();
|
|
99
|
+
// Share the bridge container's network namespace so localhost:10000 is the
|
|
100
|
+
// bridge (set by `muscat-pk dev`). Standalone dev:back has no bridge to join.
|
|
101
|
+
const bridgeContainer = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
|
|
102
|
+
const netFlags = useHostNet
|
|
103
|
+
? ["--network", "host"]
|
|
104
|
+
: bridgeContainer ? ["--network", `container:${bridgeContainer}`] : [];
|
|
161
105
|
// Only the container's OWN reverse-dial path needs to publish a listen port.
|
|
162
|
-
// With
|
|
106
|
+
// With host networking the port is already on the host; with the byte-proxy
|
|
163
107
|
// the container just connects out — neither needs -p.
|
|
164
108
|
const standaloneZenohPortFlags =
|
|
165
|
-
!
|
|
109
|
+
!useHostNet && !bridgeContainer && !proxyEndpoint && zenohPort
|
|
110
|
+
? ["-p", `${zenohPort}:${zenohPort}`]
|
|
111
|
+
: [];
|
|
112
|
+
// Standalone dev:back owns its netns, so it publishes its own declared ports.
|
|
113
|
+
// Under `dev` the bridge owns the netns and published them already.
|
|
114
|
+
const pluginPortFlags =
|
|
115
|
+
!useHostNet && !bridgeContainer ? publishPortFlags(portRanges) : [];
|
|
116
|
+
const hostGatewayFlags = useHostNet || bridgeContainer
|
|
117
|
+
? []
|
|
118
|
+
: ["--add-host", "host.docker.internal:host-gateway"];
|
|
119
|
+
// With host networking or the bridge's netns, 127.0.0.1:10000 (the SDK's
|
|
120
|
+
// gateway default) already reaches the bridge. Otherwise the container's own
|
|
121
|
+
// 127.0.0.1 is itself — host.docker.internal reaches the host.
|
|
122
|
+
const gatewayFlags = useHostNet || bridgeContainer
|
|
123
|
+
? []
|
|
124
|
+
: ["-e", "RB_PLUGIN_GATEWAY_URL=http://host.docker.internal:10000"];
|
|
166
125
|
const envFlags = [
|
|
167
126
|
// docker exec below has no -t (no pseudo-TTY), so Python defaults to
|
|
168
127
|
// block-buffered stdout — print() output wouldn't show up until the
|
|
169
128
|
// buffer fills or the process exits. Force unbuffered.
|
|
170
129
|
"-e", "PYTHONUNBUFFERED=1",
|
|
171
|
-
...
|
|
130
|
+
...gatewayFlags,
|
|
172
131
|
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
173
132
|
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
174
133
|
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
@@ -184,18 +143,19 @@ async function runPython(root, portRanges) {
|
|
|
184
143
|
const docker = (args, opts = {}) =>
|
|
185
144
|
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
186
145
|
|
|
187
|
-
// start a warm, long-lived container with the plugin mounted
|
|
188
|
-
// 호스트 uid 로 돌린다(dockerUserFlags) — root 로 쓴 build/·install/ 은 사용자가 지우지 못한다.
|
|
146
|
+
// start a warm, long-lived container with the plugin mounted
|
|
189
147
|
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
190
148
|
log.step("starting Python dev container…");
|
|
191
149
|
const up = docker(
|
|
192
|
-
["run", "-d", "--name", container, ...
|
|
150
|
+
["run", "-d", "--name", container, ...netFlags, ...standaloneZenohPortFlags, ...pluginPortFlags, ...realtimeFlags(),
|
|
151
|
+
// host.docker.internal is automatic on Docker Desktop (macOS/Windows);
|
|
152
|
+
// this makes it resolve on Linux too, for the no-bridge gateway fallback above.
|
|
153
|
+
...hostGatewayFlags,
|
|
193
154
|
"-e", "PIP_ROOT_USER_ACTION=ignore",
|
|
194
155
|
"-v", `${root}:/plugin`, "-w", "/plugin", PYTHON_DEV_IMAGE, "sleep", "infinity"],
|
|
195
156
|
{ stdio: "inherit" },
|
|
196
157
|
);
|
|
197
158
|
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
198
|
-
startMediaServer(container);
|
|
199
159
|
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [zenohPort] });
|
|
200
160
|
|
|
201
161
|
log.step("pip install requirements + watchfiles (in container)…");
|
|
@@ -413,7 +373,7 @@ runpy.run_path("/plugin/py/main.py", run_name="__main__")
|
|
|
413
373
|
PY`]);
|
|
414
374
|
|
|
415
375
|
if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
|
|
416
|
-
if (
|
|
376
|
+
if (useHostNet) log.step("network: host (native Linux — direct robot/bridge access)");
|
|
417
377
|
if (proxyEndpoint) log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
|
|
418
378
|
else if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
|
|
419
379
|
log.step("watchfiles → py/main.py (reload on any .py change in py/)");
|
|
@@ -469,14 +429,10 @@ async function runRos2(root, portRanges) {
|
|
|
469
429
|
const container = `${ROS2_DEV_IMAGE}-run`;
|
|
470
430
|
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
471
431
|
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
472
|
-
const net = backendNetwork(portRanges);
|
|
473
432
|
const envFlags = [
|
|
474
433
|
// Same rationale as runPython(): no -t on docker exec below means
|
|
475
434
|
// rclpy's Python process stdout is block-buffered, not line-buffered.
|
|
476
435
|
"-e", "PYTHONUNBUFFERED=1",
|
|
477
|
-
// Same as the robot: ROS discovery stays on this host (allow a PC with ROS_STATIC_PEERS).
|
|
478
|
-
"-e", "ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST",
|
|
479
|
-
...net.env,
|
|
480
436
|
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
481
437
|
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
482
438
|
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
@@ -489,11 +445,10 @@ async function runRos2(root, portRanges) {
|
|
|
489
445
|
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
490
446
|
log.step("starting ROS2 dev container…");
|
|
491
447
|
const up = docker(
|
|
492
|
-
["run", "-d", "--name", container, ...
|
|
448
|
+
["run", "-d", "--name", container, ...publishPortFlags(portRanges), ...realtimeFlags(), "-e", "PIP_ROOT_USER_ACTION=ignore", "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
|
|
493
449
|
{ stdio: "inherit" },
|
|
494
450
|
);
|
|
495
451
|
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
496
|
-
startMediaServer(container);
|
|
497
452
|
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
|
|
498
453
|
|
|
499
454
|
// one-time deps + symlink build (edits to .py reflect without rebuilding)
|
|
@@ -504,24 +459,15 @@ async function runRos2(root, portRanges) {
|
|
|
504
459
|
let child = null;
|
|
505
460
|
const startNode = () => {
|
|
506
461
|
if (child) child.kill();
|
|
507
|
-
|
|
508
|
-
spawnSync("docker", ["exec", container, "pkill", "-f", `${pkg}_node|${manifest.runtime?.launch || pkg}`], { stdio: "ignore" });
|
|
509
|
-
const peers = readRosPeers(root);
|
|
510
|
-
// Same command the robot runs: rclpy execs the console script, rclcpp uses `ros2 run`;
|
|
511
|
-
// relative topics land under the plugin's namespace (rosNamespace).
|
|
512
|
-
const launch = manifest.runtime?.launch || `${pkg}_node`;
|
|
513
|
-
const nsArgs = PLUGIN_ID_RE.test(pluginId) ? ` --ros-args -r __ns:=${rosNamespace(pluginId)}` : "";
|
|
514
|
-
const run = (isRclcpp ? `ros2 run ${pkg} ${launch}` : `exec ${launch}`) + nsArgs;
|
|
515
|
-
log.step(run);
|
|
462
|
+
log.step(`ros2 run ${pkg} ${pkg}_node`);
|
|
516
463
|
child = spawn(
|
|
517
464
|
"docker",
|
|
518
|
-
["exec", ...envFlags,
|
|
519
|
-
`cd ros2_ws && source install/setup.bash && ${
|
|
465
|
+
["exec", ...envFlags, container, "bash", "-lc",
|
|
466
|
+
`cd ros2_ws && source install/setup.bash && ros2 run ${pkg} ${pkg}_node`],
|
|
520
467
|
{ cwd: root, stdio: "inherit" },
|
|
521
468
|
);
|
|
522
469
|
};
|
|
523
470
|
startNode();
|
|
524
|
-
serveRosPeers(root, pluginId, startNode);
|
|
525
471
|
|
|
526
472
|
// restart node on source change. rclpy reflects .py edits live (symlink-install);
|
|
527
473
|
// rclcpp (C++) and package-metadata changes require a colcon rebuild.
|
|
@@ -567,9 +513,7 @@ async function runCpp(root, portRanges) {
|
|
|
567
513
|
const container = `${CPP_DEV_IMAGE}-run`;
|
|
568
514
|
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
569
515
|
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
570
|
-
const net = backendNetwork(portRanges);
|
|
571
516
|
const envFlags = [
|
|
572
|
-
...net.env,
|
|
573
517
|
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
574
518
|
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
575
519
|
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
@@ -582,11 +526,10 @@ async function runCpp(root, portRanges) {
|
|
|
582
526
|
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
583
527
|
log.step("starting C++ dev container…");
|
|
584
528
|
const up = docker(
|
|
585
|
-
["run", "-d", "--name", container, ...
|
|
529
|
+
["run", "-d", "--name", container, ...publishPortFlags(portRanges), ...realtimeFlags(), "-v", `${root}:/plugin`, "-w", "/plugin", CPP_DEV_IMAGE, "sleep", "infinity"],
|
|
586
530
|
{ stdio: "inherit" },
|
|
587
531
|
);
|
|
588
532
|
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
589
|
-
startMediaServer(container);
|
|
590
533
|
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
|
|
591
534
|
|
|
592
535
|
const build = () => {
|
package/cli/src/host_agent.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
"""Host-side Zenoh byte-proxy
|
|
1
|
+
"""Host-side raw Zenoh byte-proxy.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Runs on the host as a normal Zenoh peer (reverse-dials the robot, same as the
|
|
4
|
+
SDK's own connect()) and serves a tiny domain-agnostic byte protocol over TCP
|
|
5
|
+
— {query, put, subscribe} by keyexpr + bytes only — so a Docker Desktop
|
|
6
|
+
container, which cannot join the robot's Zenoh mesh itself, can talk to a
|
|
7
|
+
process that can.
|
|
5
8
|
|
|
6
9
|
Env: MUSCAT_ROBOT_HOST, MUSCAT_ROBOT_TOKEN, MUSCAT_ZENOH_ADVERTISE_HOST,
|
|
7
10
|
MUSCAT_ZENOH_PORT (robot-facing), MUSCAT_PROXY_PORT (container-facing).
|
|
@@ -9,21 +12,13 @@ MUSCAT_ZENOH_PORT (robot-facing), MUSCAT_PROXY_PORT (container-facing).
|
|
|
9
12
|
|
|
10
13
|
from __future__ import annotations
|
|
11
14
|
|
|
15
|
+
import asyncio
|
|
12
16
|
import contextlib
|
|
13
17
|
import json
|
|
14
18
|
import os
|
|
15
|
-
import signal
|
|
16
19
|
import socket
|
|
17
20
|
import struct
|
|
18
|
-
import sys
|
|
19
21
|
import threading
|
|
20
|
-
import urllib.error
|
|
21
|
-
import urllib.request
|
|
22
|
-
|
|
23
|
-
import zenoh
|
|
24
|
-
|
|
25
|
-
PEER_TTL = 30
|
|
26
|
-
QUERY_TARGETS = {0: zenoh.QueryTarget.BEST_MATCHING, 1: zenoh.QueryTarget.ALL, 2: zenoh.QueryTarget.ALL_COMPLETE}
|
|
27
22
|
|
|
28
23
|
|
|
29
24
|
def _send_frame(conn: socket.socket, lock: threading.Lock, obj: dict) -> None:
|
|
@@ -49,156 +44,102 @@ def _recv_frame(conn: socket.socket) -> dict | None:
|
|
|
49
44
|
return json.loads(buf.decode())
|
|
50
45
|
|
|
51
46
|
|
|
52
|
-
def
|
|
53
|
-
return sample.attachment.to_string() if sample.attachment is not None else ""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _query(session: zenoh.Session, req: dict) -> list[dict]:
|
|
57
|
-
payload = bytes.fromhex(req["payload_hex"]) if req.get("payload_hex") else None
|
|
58
|
-
replies = session.get(
|
|
59
|
-
req["keyexpr"],
|
|
60
|
-
payload=payload,
|
|
61
|
-
timeout=req.get("timeout_ms", 2000) / 1000,
|
|
62
|
-
target=QUERY_TARGETS.get(req.get("target", 0), zenoh.QueryTarget.BEST_MATCHING),
|
|
63
|
-
)
|
|
64
|
-
out = []
|
|
65
|
-
for reply in replies:
|
|
66
|
-
if reply.ok is not None:
|
|
67
|
-
s = reply.ok
|
|
68
|
-
out.append({"key": str(s.key_expr), "payload_hex": s.payload.to_bytes().hex(),
|
|
69
|
-
"att": _att(s), "encoding": str(s.encoding), "is_error": False})
|
|
70
|
-
else:
|
|
71
|
-
e = reply.err
|
|
72
|
-
out.append({"key": req["keyexpr"], "payload_hex": e.payload.to_bytes().hex(),
|
|
73
|
-
"att": "", "encoding": str(e.encoding), "is_error": True})
|
|
74
|
-
return out
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _handle_conn(conn: socket.socket, session: zenoh.Session) -> None:
|
|
47
|
+
def _handle_conn(conn: socket.socket, zenoh_client) -> None:
|
|
78
48
|
send_lock = threading.Lock()
|
|
79
|
-
|
|
49
|
+
sub_ids: dict[str, int] = {}
|
|
80
50
|
try:
|
|
81
51
|
while True:
|
|
82
52
|
req = _recv_frame(conn)
|
|
83
53
|
if req is None:
|
|
84
54
|
return
|
|
85
55
|
op = req.get("type")
|
|
86
|
-
rid = req.get("id")
|
|
87
56
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
57
|
+
if op == "query":
|
|
58
|
+
payload = bytes.fromhex(req["payload_hex"]) if req.get("payload_hex") else None
|
|
59
|
+
try:
|
|
60
|
+
replies = zenoh_client._client.query(
|
|
61
|
+
req["keyexpr"], payload, req.get("timeout_ms", 2000), req.get("target", 0)
|
|
62
|
+
)
|
|
63
|
+
out = {
|
|
64
|
+
"type": "query_resp", "id": req["id"], "ok": True,
|
|
65
|
+
"replies": [
|
|
66
|
+
{"key": k, "payload_hex": bytes(p).hex(), "att": a, "encoding": e, "is_error": ie}
|
|
67
|
+
for (k, p, a, e, ie) in replies
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
out = {"type": "query_resp", "id": req["id"], "ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
72
|
+
_send_frame(conn, send_lock, out)
|
|
73
|
+
|
|
74
|
+
elif op == "put":
|
|
75
|
+
payload = bytes.fromhex(req["payload_hex"]) if req.get("payload_hex") else b""
|
|
76
|
+
try:
|
|
77
|
+
zenoh_client._client.publish(req["keyexpr"], payload, "", 0, 5, False)
|
|
78
|
+
out = {"type": "put_resp", "id": req["id"], "ok": True}
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
out = {"type": "put_resp", "id": req["id"], "ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
81
|
+
_send_frame(conn, send_lock, out)
|
|
82
|
+
|
|
83
|
+
elif op == "subscribe":
|
|
84
|
+
keyexpr = req["keyexpr"]
|
|
85
|
+
|
|
86
|
+
def on_sample(topic: str, payload_bytes: bytes, att: str, _ke: str = keyexpr) -> None:
|
|
87
|
+
_send_frame(conn, send_lock, {
|
|
88
|
+
"type": "sample", "keyexpr": _ke, "key": topic,
|
|
89
|
+
"payload_hex": bytes(payload_bytes).hex(), "att": att,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
sub_id = zenoh_client._client.declare_subscriber(keyexpr, on_sample)
|
|
94
|
+
sub_ids[keyexpr] = sub_id
|
|
95
|
+
out = {"type": "subscribe_resp", "id": req["id"], "ok": True}
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
out = {"type": "subscribe_resp", "id": req["id"], "ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
98
|
+
_send_frame(conn, send_lock, out)
|
|
99
|
+
|
|
100
|
+
elif op == "unsubscribe":
|
|
101
|
+
keyexpr = req["keyexpr"]
|
|
102
|
+
sub_id = sub_ids.pop(keyexpr, None)
|
|
103
|
+
if sub_id is not None:
|
|
104
|
+
try:
|
|
105
|
+
zenoh_client._client.undeclare_subscriber(sub_id)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
_send_frame(conn, send_lock, {"type": "unsubscribe_resp", "id": req["id"], "ok": True})
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
_send_frame(conn, send_lock, {"type": "error", "error": f"unknown op {op!r}"})
|
|
124
112
|
except (ConnectionResetError, BrokenPipeError, OSError):
|
|
125
113
|
pass
|
|
126
114
|
finally:
|
|
127
|
-
for
|
|
128
|
-
with contextlib.suppress(Exception):
|
|
129
|
-
sub.undeclare()
|
|
130
|
-
conn.close()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class Peer:
|
|
134
|
-
"""Registers this host with the robot's common: POST /zenoh/peer, heartbeat, DELETE."""
|
|
135
|
-
|
|
136
|
-
def __init__(self, host: str, token: str, ip: str, port: int) -> None:
|
|
137
|
-
self.base = f"http://{host}:8000"
|
|
138
|
-
self.headers = {"Content-Type": "application/json"}
|
|
139
|
-
if token:
|
|
140
|
-
self.headers["Authorization"] = f"Bearer {token}"
|
|
141
|
-
self.body = {"ip": ip, "port": port, "ttl": PEER_TTL}
|
|
142
|
-
self.peer_id: str | None = None
|
|
143
|
-
self.stop = threading.Event()
|
|
144
|
-
|
|
145
|
-
def _http(self, method: str, path: str, body: dict | None = None) -> dict:
|
|
146
|
-
data = json.dumps(body).encode() if body is not None else None
|
|
147
|
-
req = urllib.request.Request(self.base + path, data=data, headers=self.headers, method=method)
|
|
148
|
-
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
149
|
-
raw = resp.read()
|
|
150
|
-
return json.loads(raw) if raw else {}
|
|
151
|
-
|
|
152
|
-
def register(self) -> None:
|
|
153
|
-
self.peer_id = self._http("POST", "/zenoh/peer", self.body)["peer_id"]
|
|
154
|
-
print(f"[host-agent] registered with {self.base} as zenoh peer {self.peer_id}", flush=True)
|
|
155
|
-
|
|
156
|
-
def maintain(self) -> None:
|
|
157
|
-
while True:
|
|
158
|
-
wait = PEER_TTL / 2 if self.peer_id else 3
|
|
159
|
-
if self.stop.wait(wait):
|
|
160
|
-
return
|
|
115
|
+
for sub_id in sub_ids.values():
|
|
161
116
|
try:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
self.peer_id = None
|
|
167
|
-
except Exception as exc:
|
|
168
|
-
print(f"[host-agent] peer maintenance failed ({exc}); retrying", flush=True)
|
|
169
|
-
if self.peer_id is not None and isinstance(exc, urllib.error.HTTPError) and exc.code == 404:
|
|
170
|
-
self.peer_id = None
|
|
171
|
-
|
|
172
|
-
def close(self) -> None:
|
|
173
|
-
self.stop.set()
|
|
174
|
-
if self.peer_id:
|
|
175
|
-
with contextlib.suppress(Exception):
|
|
176
|
-
self._http("DELETE", f"/zenoh/peer/{self.peer_id}")
|
|
117
|
+
zenoh_client._client.undeclare_subscriber(sub_id)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
conn.close()
|
|
177
121
|
|
|
178
122
|
|
|
179
123
|
def main() -> None:
|
|
180
|
-
|
|
181
|
-
|
|
124
|
+
from rainbow.rb_sdk.base import RBBaseSDK
|
|
125
|
+
|
|
182
126
|
host = os.environ["MUSCAT_ROBOT_HOST"]
|
|
183
127
|
token = os.environ.get("MUSCAT_ROBOT_TOKEN", "")
|
|
184
128
|
advertise = os.environ["MUSCAT_ZENOH_ADVERTISE_HOST"]
|
|
185
129
|
zenoh_port = int(os.environ["MUSCAT_ZENOH_PORT"])
|
|
186
130
|
proxy_port = int(os.environ["MUSCAT_PROXY_PORT"])
|
|
187
131
|
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
conf.insert_json5("listen/endpoints", json.dumps([f"tcp/0.0.0.0:{zenoh_port}"]))
|
|
192
|
-
conf.insert_json5("scouting/multicast/enabled", "false")
|
|
193
|
-
session = zenoh.open(conf)
|
|
132
|
+
# The robot reverse-dials the advertised host:port; force it to the host's
|
|
133
|
+
# real LAN IP (matches what dev.mjs computed via ensureRemoteZenohEnv).
|
|
134
|
+
RBBaseSDK._detect_local_ip = staticmethod(lambda _host: advertise)
|
|
194
135
|
|
|
195
|
-
peer =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
print(f"[host-agent] zenoh peer listening :{zenoh_port}
|
|
136
|
+
peer = RBBaseSDK()
|
|
137
|
+
connect_kwargs = {"host": host, "zenoh_port": zenoh_port, "mesh_timeout": 15.0}
|
|
138
|
+
if token:
|
|
139
|
+
connect_kwargs["token"] = token
|
|
140
|
+
|
|
141
|
+
asyncio.run(peer.connect(**connect_kwargs))
|
|
142
|
+
print(f"[host-agent] zenoh peer joined robot mesh (listening :{zenoh_port})", flush=True)
|
|
202
143
|
|
|
203
144
|
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
204
145
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
@@ -209,13 +150,12 @@ def main() -> None:
|
|
|
209
150
|
try:
|
|
210
151
|
while True:
|
|
211
152
|
conn, _addr = srv.accept()
|
|
212
|
-
threading.Thread(target=_handle_conn, args=(conn,
|
|
153
|
+
threading.Thread(target=_handle_conn, args=(conn, peer.zenoh_client), daemon=True).start()
|
|
213
154
|
except KeyboardInterrupt:
|
|
214
155
|
pass
|
|
215
156
|
finally:
|
|
216
|
-
peer.close()
|
|
217
157
|
with contextlib.suppress(Exception):
|
|
218
|
-
|
|
158
|
+
asyncio.run(peer.disconnect())
|
|
219
159
|
|
|
220
160
|
|
|
221
161
|
if __name__ == "__main__":
|
package/cli/src/manifest.mjs
CHANGED
|
@@ -11,15 +11,6 @@ export const CONTRIBUTION_POINTS = [
|
|
|
11
11
|
"program.panel",
|
|
12
12
|
];
|
|
13
13
|
const ICON_SIZES = ["16", "48", "128"];
|
|
14
|
-
// Reverse-domain id: "."-separated parts of lowercase letters/digits joined by single "-",
|
|
15
|
-
// each part starting with a letter. Keeps the ROS namespace derived from it (rosNamespace)
|
|
16
|
-
// valid and collision-free (no "_" in ids, so "." and "-" map to distinct characters).
|
|
17
|
-
export const PLUGIN_ID_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)*$/;
|
|
18
|
-
|
|
19
|
-
/** ROS namespace for a plugin id: "." → "/", "-" → "_" (com.example.my-plugin → /com/example/my_plugin). */
|
|
20
|
-
export function rosNamespace(id) {
|
|
21
|
-
return "/" + id.replace(/\./g, "/").replace(/-/g, "_");
|
|
22
|
-
}
|
|
23
14
|
const RUNTIME_TYPES = ["none", "python", "cpp", "ros2"];
|
|
24
15
|
const ROS_CLIENTS = ["rclpy", "rclcpp"];
|
|
25
16
|
const I18N_LANGS = ["ko", "en", "ja", "zh", "vi", "th", "es"];
|
|
@@ -201,8 +192,6 @@ export function validateManifest(manifest) {
|
|
|
201
192
|
if (typeof manifest[key] !== "string" || manifest[key].length === 0)
|
|
202
193
|
errors.push(`"${key}" is required`);
|
|
203
194
|
}
|
|
204
|
-
if (typeof manifest.id === "string" && manifest.id && !PLUGIN_ID_RE.test(manifest.id))
|
|
205
|
-
errors.push(`"id" must be "."-separated parts of lowercase letters/digits and single "-", each starting with a letter (e.g. com.example.my-plugin)`);
|
|
206
195
|
if (typeof manifest.author !== "string" || manifest.author.length === 0)
|
|
207
196
|
errors.push(`"author" is required`);
|
|
208
197
|
if (typeof manifest.email !== "string" || manifest.email.length === 0)
|