@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.51 → 0.1.2-dev.52
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/src/dev.mjs +105 -4
- package/cli/src/devBack.mjs +78 -18
- package/cli/storybook-dist/project.json +1 -1
- package/package.json +3 -3
package/cli/src/dev.mjs
CHANGED
|
@@ -1,11 +1,105 @@
|
|
|
1
1
|
// muscat-pk dev — run the Socket.IO bridge (Docker), dev:ui, and dev:back together.
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { release } from "node:os";
|
|
5
|
+
import { resolve } from "node:path";
|
|
2
6
|
import { runDevUi } from "./devUi.mjs";
|
|
3
7
|
import { runDevBack, stopDevBackContainers } from "./devBack.mjs";
|
|
4
8
|
import { startBridge, stopBridge, BRIDGE_CONTAINER, BRIDGE_PORT } from "./bridge.mjs";
|
|
5
9
|
import { assertManifestPorts } from "./manifest.mjs";
|
|
6
10
|
import { formatPortRanges } from "./ports.mjs";
|
|
7
|
-
import {
|
|
8
|
-
|
|
11
|
+
import {
|
|
12
|
+
ensureDocker,
|
|
13
|
+
ensureIdeVenv,
|
|
14
|
+
findPluginRoot,
|
|
15
|
+
log,
|
|
16
|
+
prompt,
|
|
17
|
+
promptRobotConnection,
|
|
18
|
+
venvPython,
|
|
19
|
+
} from "./util.mjs";
|
|
20
|
+
import { detectRouteAddress, ensureRemoteZenohEnv, getFreePort, isRemoteRobot } from "./zenohDev.mjs";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_HOST_AGENT_PORT = 18080;
|
|
23
|
+
const isWsl = () => process.platform === "linux" && /microsoft|wsl/i.test(release());
|
|
24
|
+
const canAutoStartHostAgent = () => process.platform === "darwin" || isWsl();
|
|
25
|
+
|
|
26
|
+
function waitForHostAgent(port, { timeoutMs = 30_000 } = {}) {
|
|
27
|
+
const deadline = Date.now() + timeoutMs;
|
|
28
|
+
const url = `http://127.0.0.1:${port}/health`;
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const probe = async () => {
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(url);
|
|
33
|
+
if (res.ok) return resolve(true);
|
|
34
|
+
} catch {
|
|
35
|
+
// keep polling
|
|
36
|
+
}
|
|
37
|
+
if (Date.now() >= deadline) {
|
|
38
|
+
reject(new Error(`host SDK agent did not become ready at ${url}`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
setTimeout(probe, 300);
|
|
42
|
+
};
|
|
43
|
+
probe();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function maybeStartHostAgent(root, { robotIp, robotToken }) {
|
|
48
|
+
if (process.env.MUSCAT_HOST_AGENT_URL) {
|
|
49
|
+
log.step(`host SDK agent → ${process.env.MUSCAT_HOST_AGENT_URL}`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (process.env.MUSCAT_HOST_AGENT_DISABLE === "1") return null;
|
|
53
|
+
if (!canAutoStartHostAgent() || !isRemoteRobot(robotIp)) return null;
|
|
54
|
+
|
|
55
|
+
const agentPath = resolve(root, "py", "host_agent.py");
|
|
56
|
+
if (!existsSync(agentPath)) return null;
|
|
57
|
+
|
|
58
|
+
const wsl = isWsl();
|
|
59
|
+
if (wsl) await ensureRemoteZenohEnv(robotIp, { log });
|
|
60
|
+
|
|
61
|
+
const bindHost = wsl ? "0.0.0.0" : "127.0.0.1";
|
|
62
|
+
const containerHost = wsl
|
|
63
|
+
? process.env.MUSCAT_HOST_AGENT_CONTAINER_HOST || await detectRouteAddress(robotIp)
|
|
64
|
+
: "host.docker.internal";
|
|
65
|
+
|
|
66
|
+
if (!containerHost) {
|
|
67
|
+
log.warn("host SDK agent skipped: could not determine container-reachable host");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
log.step("starting host SDK agent (Docker Desktop workaround)...");
|
|
72
|
+
if (!ensureIdeVenv(root)) {
|
|
73
|
+
log.warn("host SDK agent skipped: could not prepare .venv");
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const port = await getFreePort(Number(process.env.MUSCAT_HOST_AGENT_PORT || DEFAULT_HOST_AGENT_PORT));
|
|
78
|
+
const py = venvPython(resolve(root, ".venv"));
|
|
79
|
+
const child = spawn(py, [agentPath], {
|
|
80
|
+
cwd: root,
|
|
81
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
82
|
+
env: {
|
|
83
|
+
...process.env,
|
|
84
|
+
MUSCAT_ROBOT_HOST: robotIp,
|
|
85
|
+
MUSCAT_ROBOT_TOKEN: robotToken,
|
|
86
|
+
MUSCAT_HOST_AGENT_BIND: bindHost,
|
|
87
|
+
MUSCAT_HOST_AGENT_PORT: String(port),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await waitForHostAgent(port);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
child.kill();
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const containerUrl = `http://${containerHost}:${port}`;
|
|
99
|
+
process.env.MUSCAT_HOST_AGENT_URL = containerUrl;
|
|
100
|
+
log.step(`host SDK agent → ${containerUrl}`);
|
|
101
|
+
return child;
|
|
102
|
+
}
|
|
9
103
|
|
|
10
104
|
export async function runDev(args = []) {
|
|
11
105
|
const root = findPluginRoot();
|
|
@@ -20,13 +114,19 @@ export async function runDev(args = []) {
|
|
|
20
114
|
if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
|
|
21
115
|
|
|
22
116
|
const remoteRobot = isRemoteRobot(robotIp);
|
|
117
|
+
const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
|
|
118
|
+
const hostAgentUrl = process.env.MUSCAT_HOST_AGENT_URL || "";
|
|
23
119
|
// Compute the Zenoh advertise host/port up front — startBridge() needs the
|
|
24
120
|
// port to publish it on the bridge container so the robot can reach back.
|
|
25
|
-
|
|
121
|
+
if (hostAgentUrl && !hostAgent) {
|
|
122
|
+
log.step(`host SDK agent → ${hostAgentUrl}`);
|
|
123
|
+
} else if (!hostAgentUrl) {
|
|
124
|
+
await ensureRemoteZenohEnv(robotIp, { log });
|
|
125
|
+
}
|
|
26
126
|
|
|
27
127
|
log.step("starting Socket.IO bridge (Docker) + dev:ui + dev:back");
|
|
28
128
|
const bridgeStarted = await startBridge({
|
|
29
|
-
zenohPort: remoteRobot ? Number(process.env.MUSCAT_ZENOH_PORT) : null,
|
|
129
|
+
zenohPort: remoteRobot && !hostAgentUrl ? Number(process.env.MUSCAT_ZENOH_PORT) : null,
|
|
30
130
|
});
|
|
31
131
|
|
|
32
132
|
if (!bridgeStarted) {
|
|
@@ -43,6 +143,7 @@ export async function runDev(args = []) {
|
|
|
43
143
|
process.env.MUSCAT_PK_MANAGED_DEV = "1";
|
|
44
144
|
|
|
45
145
|
const cleanup = () => {
|
|
146
|
+
if (hostAgent && hostAgent.exitCode === null) hostAgent.kill();
|
|
46
147
|
stopDevBackContainers();
|
|
47
148
|
if (bridgeStarted) stopBridge();
|
|
48
149
|
};
|
package/cli/src/devBack.mjs
CHANGED
|
@@ -87,6 +87,7 @@ async function runPython(root, portRanges) {
|
|
|
87
87
|
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
88
88
|
const zenohAdvertiseHost = process.env.MUSCAT_ZENOH_ADVERTISE_HOST ?? "";
|
|
89
89
|
const zenohPort = process.env.MUSCAT_ZENOH_PORT ?? "";
|
|
90
|
+
const hostAgentUrl = process.env.MUSCAT_HOST_AGENT_URL ?? "";
|
|
90
91
|
// Share the bridge container's network namespace so localhost:10000 is the
|
|
91
92
|
// bridge (set by `muscat-pk dev`). Standalone dev:back has no bridge to join.
|
|
92
93
|
const bridgeContainer = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
|
|
@@ -112,8 +113,9 @@ async function runPython(root, portRanges) {
|
|
|
112
113
|
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
113
114
|
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
114
115
|
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
115
|
-
...(zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
|
|
116
|
-
...(zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
|
|
116
|
+
...(!hostAgentUrl && zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
|
|
117
|
+
...(!hostAgentUrl && zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
|
|
118
|
+
...(hostAgentUrl ? ["-e", `MUSCAT_HOST_AGENT_URL=${hostAgentUrl}`] : []),
|
|
117
119
|
...portEnvFlags(portRanges),
|
|
118
120
|
];
|
|
119
121
|
const docker = (args, opts = {}) =>
|
|
@@ -140,34 +142,90 @@ async function runPython(root, portRanges) {
|
|
|
140
142
|
" && { [ -f requirements.txt ] && pip install --break-system-packages --ignore-installed -q -r requirements.txt || true; }"]);
|
|
141
143
|
|
|
142
144
|
docker(["exec", container, "bash", "-lc", `cat > /tmp/muscat-pk-python-dev-entry.py <<'PY'
|
|
145
|
+
import json
|
|
143
146
|
import os
|
|
144
|
-
import
|
|
147
|
+
import urllib.request
|
|
145
148
|
|
|
146
|
-
|
|
149
|
+
def install_host_agent_sdk_patch(host_agent_url):
|
|
147
150
|
from rainbow.rb_sdk.base import RBBaseSDK
|
|
151
|
+
from rainbow.rb_sdk.manipulate_sdk.manipulate_move import RBManipulateMoveSDK
|
|
152
|
+
|
|
153
|
+
base_url = host_agent_url.rstrip("/")
|
|
154
|
+
|
|
155
|
+
def post_json(path, payload):
|
|
156
|
+
req = urllib.request.Request(
|
|
157
|
+
f"{base_url}{path}",
|
|
158
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
159
|
+
headers={"Content-Type": "application/json"},
|
|
160
|
+
method="POST",
|
|
161
|
+
)
|
|
162
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
163
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
164
|
+
if not data.get("ok"):
|
|
165
|
+
raise RuntimeError(data.get("error") or "host SDK agent request failed")
|
|
166
|
+
return data.get("result")
|
|
167
|
+
|
|
168
|
+
async def connect_via_host_agent(self, *args, **kwargs):
|
|
169
|
+
print(f"[muscat-pk] SDK connect routed through host agent: {base_url}", flush=True)
|
|
170
|
+
return "host-agent"
|
|
171
|
+
|
|
172
|
+
async def disconnect_via_host_agent(self, *args, **kwargs):
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def call_move_j_via_host_agent(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
robot_model,
|
|
179
|
+
target,
|
|
180
|
+
reference_value=None,
|
|
181
|
+
speed=None,
|
|
182
|
+
flow_manager_args=None,
|
|
183
|
+
):
|
|
184
|
+
result = post_json("/move_j", {
|
|
185
|
+
"robot_model": robot_model,
|
|
186
|
+
"values": target.get("tar_values") if isinstance(target, dict) else None,
|
|
187
|
+
"target": target,
|
|
188
|
+
"reference_value": reference_value,
|
|
189
|
+
"speed": speed,
|
|
190
|
+
})
|
|
191
|
+
if flow_manager_args is not None:
|
|
192
|
+
flow_manager_args.done()
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
RBBaseSDK.connect = connect_via_host_agent
|
|
196
|
+
RBBaseSDK.disconnect = disconnect_via_host_agent
|
|
197
|
+
RBManipulateMoveSDK.call_move_j = call_move_j_via_host_agent
|
|
148
198
|
|
|
199
|
+
try:
|
|
149
200
|
advertise_host = os.getenv("MUSCAT_ZENOH_ADVERTISE_HOST")
|
|
150
201
|
zenoh_port = os.getenv("MUSCAT_ZENOH_PORT")
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
202
|
+
host_agent_url = os.getenv("MUSCAT_HOST_AGENT_URL")
|
|
203
|
+
|
|
204
|
+
if host_agent_url:
|
|
205
|
+
print(f"[muscat-pk] host SDK agent mode: {host_agent_url}", flush=True)
|
|
206
|
+
install_host_agent_sdk_patch(host_agent_url)
|
|
207
|
+
else:
|
|
208
|
+
from rainbow.rb_sdk.base import RBBaseSDK
|
|
209
|
+
if advertise_host:
|
|
210
|
+
RBBaseSDK._detect_local_ip = staticmethod(lambda host: advertise_host)
|
|
211
|
+
if zenoh_port:
|
|
212
|
+
_orig_connect = RBBaseSDK.connect
|
|
213
|
+
|
|
214
|
+
async def _connect_with_dev_port(self, *args, **kwargs):
|
|
215
|
+
kwargs.setdefault("zenoh_port", int(zenoh_port))
|
|
216
|
+
return await _orig_connect(self, *args, **kwargs)
|
|
217
|
+
|
|
218
|
+
RBBaseSDK.connect = _connect_with_dev_port
|
|
163
219
|
except Exception as exc:
|
|
164
220
|
print(f"[muscat-pk] SDK dev network patch skipped: {exc}", flush=True)
|
|
165
221
|
|
|
222
|
+
import runpy
|
|
166
223
|
runpy.run_path("/plugin/py/main.py", run_name="__main__")
|
|
167
224
|
PY`]);
|
|
168
225
|
|
|
169
226
|
if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
|
|
170
|
-
if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
|
|
227
|
+
if (!hostAgentUrl && zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
|
|
228
|
+
if (hostAgentUrl) log.step(`host SDK agent → ${hostAgentUrl}`);
|
|
171
229
|
log.step("watchfiles → py/main.py (reload on any .py change in py/)");
|
|
172
230
|
const child = spawn(
|
|
173
231
|
"docker",
|
|
@@ -387,7 +445,9 @@ export async function runDevBack(args = []) {
|
|
|
387
445
|
if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
|
|
388
446
|
// Standalone `dev:back` against a remote robot needs the Zenoh reverse-path
|
|
389
447
|
// advertise host/port too (idempotent — `muscat-pk dev` already set these).
|
|
390
|
-
|
|
448
|
+
// Host-agent mode routes robot SDK calls through the mac host, so the Docker
|
|
449
|
+
// backend does not need its own remote Zenoh peer.
|
|
450
|
+
if (!process.env.MUSCAT_HOST_AGENT_URL) await ensureRemoteZenohEnv(robotIp, { log });
|
|
391
451
|
const type = manifestRuntime(root);
|
|
392
452
|
if (type === "ros2") return await runRos2(root, portRanges);
|
|
393
453
|
if (type === "cpp") return await runCpp(root, portRanges);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"generatedAt":
|
|
1
|
+
{"generatedAt":1783659929067,"userSince":1750932757926,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":true,"refCount":0,"testPackages":{"@vitest/browser":"3.2.4","@vitest/coverage-v8":"3.2.4","playwright":"1.59.1","vitest":"3.2.4"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"yarn","version":"berry","agent":"yarn@berry"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"8.6.18","storybookVersionSpecifier":"^8.6.4","language":"typescript","storybookPackages":{"@chromatic-com/storybook":{"version":"3.2.7"},"@storybook/addon-essentials":{"version":"8.6.18"},"@storybook/addon-links":{"version":"8.6.18"},"@storybook/addon-onboarding":{"version":"8.6.18"},"@storybook/addon-themes":{"version":"8.6.18"},"@storybook/blocks":{"version":"8.6.18"},"@storybook/experimental-addon-test":{"version":"8.6.18"},"@storybook/react":{"version":"8.6.18"},"@storybook/react-vite":{"version":"8.6.18"},"@storybook/test":{"version":"8.6.18"},"@storybook/theming":{"version":"8.6.18"},"eslint-plugin-storybook":{"version":"0.11.6"},"storybook":{"version":"8.6.18"},"storybook-addon-react-docgen":{"version":"1.2.44"}},"addons":{"$SNIP/node_modules/@storybook/addon-links":{"version":null},"$SNIP/node_modules/@storybook/addon-essentials":{"version":null},"$SNIP/node_modules/@chromatic-com/storybook":{"version":null},"$SNIP/node_modules/@storybook/addon-themes":{"version":null},"$SNIP/node_modules/@storybook/experimental-addon-test":{"version":null}}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainbow-robotics/muscat-plugin-kit",
|
|
3
|
-
"version": "0.1.2-dev.
|
|
3
|
+
"version": "0.1.2-dev.52",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20"
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
56
|
-
"@rainbow-robotics/plugin-ui": "0.1.2-dev.
|
|
57
|
-
"@rainbow-robotics/preview-host": "0.1.2-dev.
|
|
56
|
+
"@rainbow-robotics/plugin-ui": "0.1.2-dev.52",
|
|
57
|
+
"@rainbow-robotics/preview-host": "0.1.2-dev.52",
|
|
58
58
|
"postcss": "^8.5.8",
|
|
59
59
|
"socket.io": "^4.8.1",
|
|
60
60
|
"typescript": "^5.8.0",
|