@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.52 → 0.1.2-dev.54
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 +31 -0
- package/cli/src/__pycache__/host_agent.cpython-37.pyc +0 -0
- package/cli/src/dev.mjs +53 -47
- package/cli/src/devBack.mjs +207 -73
- package/cli/src/host_agent.py +162 -0
- package/cli/src/new.mjs +60 -16
- package/cli/src/util.mjs +17 -7
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-BjuzF3Je.js → Color-YHDXOIA2-D7vQYxkG.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-By6k6vcM.js → Docs-Bsg6oI0R.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BQesYW-Q.js → Docs-Ct3aNuIU.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BAFZYTMR.js → Docs-D1_8pNWH.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-GoNbntAU.js → Docs-DLLbm7c2.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-D1J63cJc.js → Docs-FeoDh_a5.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BsMEe093.js → Docs-xXGAdLZ1.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-BwY_WiM7.js → DocsRenderer-CFRXHY34-BOFUtZNA.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-CvzBHfYa.js → NxAvatar.stories-bKhuOBQt.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-B83PesgN.js → NxButton.stories-BazSMSB9.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-DpNcCG6B.js → NxNumberInput.stories-BKtewPAl.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-Xnvd3xhH.js → NxPagination.stories-DWtW9y0B.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-C3YSIpM0.js → NxPointerHoldButton.stories-D1yG_ed7.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-DIhaNZig.js → NxRangeInput.stories-CsDvlzeq.js} +1 -1
- package/cli/storybook-dist/assets/{NxSelectBox.stories-BGwO2mYJ.js → NxSelectBox.stories-Bj-4JnLl.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-BXlt5Bgt.js → NxSlideRange.stories-DW0aDVjy.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-C5ZqIRtw.js → NxSwitch.stories-CnSdIJJH.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-hm_ycALo.js → NxTextArea.stories-BvmaEcbI.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-DkZAySr_.js → NxTextInput.stories-BoSG4q_t.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories--bY36GZY.js → RBAccordion.stories-Dd8RcWwh.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-BVr1ccI4.js → RBAccordionGroup.stories-C98llwq7.js} +1 -1
- package/cli/storybook-dist/assets/{RBAlert.stories-CkjFBEkv.js → RBAlert.stories-BS8qWxoE.js} +1 -1
- package/cli/storybook-dist/assets/{RBBadge.stories-CNXILDPh.js → RBBadge.stories-CfBlM7hg.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-AokMv8ZY.js → RBBattery.stories-CSmuI5nl.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-CX1sLKB2.js → RBCard.stories-DIxNHrse.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckBox.stories-By3QHBFm.js → RBCheckBox.stories-Dqj6Z2i6.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-Dud1uK3X.js → RBCheckIconButton.stories-DWIcoBrc.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-C8iBkZxY.js → RBCheckLabelButton.stories-CTpUPwcW.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipTab.stories-DsgbiS2U.js → RBChipTab.stories-DjhlY193.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-C3DZGmq0.js → RBChipsBoxTab.stories-B0ti0yqO.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-CusR20qk.js → RBColorIcons.stories-C57_4hsC.js} +1 -1
- package/cli/storybook-dist/assets/{RBCommand.stories-DhJNgaLZ.js → RBCommand.stories-BLnt9QoS.js} +1 -1
- package/cli/storybook-dist/assets/{RBConfirm.stories-B6LKpCDa.js → RBConfirm.stories-BP_CNk6g.js} +1 -1
- package/cli/storybook-dist/assets/{RBDateInput.stories-PEEhQ1wJ.js → RBDateInput.stories-wNLr_ibI.js} +1 -1
- package/cli/storybook-dist/assets/{RBDialog.stories-CAtOdRen.js → RBDialog.stories-BcOL9VOY.js} +1 -1
- package/cli/storybook-dist/assets/{RBDivider.stories-0zpIUlXT.js → RBDivider.stories-C96BjSkz.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-CWMG4glY.js → RBDrawer.stories-BR8f14ju.js} +1 -1
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-6fbEYZfk.js → RBDropdownMenu.stories-D4iI3QxW.js} +1 -1
- package/cli/storybook-dist/assets/{RBEmptyView.stories-BRDZHjn4.js → RBEmptyView.stories-DEwB6zT1.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-sYrRLYob.js → RBIconButton.stories-dbRupSvJ.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-DFe8O1_g.js → RBIcons.stories-0kc9-Q26.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-DX4VdUfk.js → RBInlineLabelButton.stories-CGqd4_cL.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-SA2Foelf.js → RBJoyStick.stories-Dl-kULKt.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-CicBns_Y.js → RBLabelButton.stories-BhN8gjBn.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-WlP3oEmY.js → RBLight.stories-CzC9JzRP.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenuTreeIcons.stories-CrWjPfxj.js → RBMenuTreeIcons.stories-DGusVthU.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-Di4phWw7.js → RBMenubar.stories-Yh20C6VS.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DS3wrR3l.js → RBMultiRangeInput.stories-CpXq7aM6.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-BDVPAUVD.js → RBPagination.stories-Czbdiw09.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-DL9b_iX9.js → RBPopover.stories-BtgnoNuU.js} +1 -1
- package/cli/storybook-dist/assets/{RBProgramIcons.stories-ByRVx-on.js → RBProgramIcons.stories-jkH3y-wm.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-vTmKWyA5.js → RBPrompt.stories-UtMlNbpB.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-C9WHt4jb.js → RBRadioBox.stories-4G6T3iSt.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-D8KYq63j.js → RBRadioButton.stories-B6MqVYZZ.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-Bvcf4Su7.js → RBRangeDateInput.stories-CGO_sq_d.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-3O2OMuH_.js → RBResizable.stories-DeEC_X7f.js} +1 -1
- package/cli/storybook-dist/assets/{RBSelectBox.stories-CKZLmPzX.js → RBSelectBox.stories-CoJt9DP8.js} +1 -1
- package/cli/storybook-dist/assets/{RBSheet.stories-Dko0It1W.js → RBSheet.stories-DPWGMnHU.js} +1 -1
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-CUvqVtId.js → RBSideNumberInput.stories-8U_Kln9e.js} +1 -1
- package/cli/storybook-dist/assets/{RBSlider.stories-CcbaKRYo.js → RBSlider.stories-BV7pwtuH.js} +1 -1
- package/cli/storybook-dist/assets/{RBSpinner.stories-D15e_JlE.js → RBSpinner.stories-B3Of2adu.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-C4JRsYJ6.js → RBSwitch.stories-CfiQSSSv.js} +1 -1
- package/cli/storybook-dist/assets/{RBTab.stories-BPvzQSM-.js → RBTab.stories-SdMNBCv9.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-nr8OS1mV.js → RBTable.stories-nvdmKDjY.js} +1 -1
- package/cli/storybook-dist/assets/{RBTableRow.stories-rI0rK8Ci.js → RBTableRow.stories-B7wI0Ee7.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-CxMtHGWA.js → RBTabs.stories-oQGtDj1P.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextArea.stories-Bru4AhJ3.js → RBTextArea.stories-CjJPMszr.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextField.stories-Ah3D9lto.js → RBTextField.stories-DLJCPXKs.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextInput.stories-C3qAVngJ.js → RBTextInput.stories-BrITKTev.js} +1 -1
- package/cli/storybook-dist/assets/{RBToast.stories-C1occsvf.js → RBToast.stories-zZkkPpIy.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-BNrNERc7.js → RBToggle.stories-gNxsk8Fe.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-D_RRBwxY.js → RBToggleGroup.stories-D1QZBlWO.js} +1 -1
- package/cli/storybook-dist/assets/{RBTooltip.stories-DuyOQqrB.js → RBTooltip.stories-C6X_sE-H.js} +1 -1
- package/cli/storybook-dist/assets/{createConveyorBeltLayer-Cn41pFRH.js → createConveyorBeltLayer-BHP9oJgS.js} +455 -408
- package/cli/storybook-dist/assets/{entry-preview-D-mLV0cS.js → entry-preview-DW9puiK6.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-DNru2eN7.js → iframe-B_vpO0M_.js} +2 -2
- package/cli/storybook-dist/assets/{index-Cwr2bQOB.js → index--B5WyAVV.js} +3 -3
- package/cli/storybook-dist/assets/{preview-QAsoD6Sa.js → preview-CQvEUp-g.js} +2 -2
- package/cli/storybook-dist/assets/{preview-rcRMbKPr.js → preview-CkOylu04.js} +1 -1
- package/cli/storybook-dist/assets/preview-Com4scch.css +1 -0
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/package.json +3 -3
- package/cli/storybook-dist/assets/preview-DUTcCPBe.css +0 -1
package/README.md
CHANGED
|
@@ -129,6 +129,37 @@ Could not create .venv (no local Python/uv?) — skipping, Docker dev still work
|
|
|
129
129
|
|
|
130
130
|
Install `uv` (recommended) or a system Python 3.12, then re-run `muscat-pk new` (or `muscat-pk type change`) to create it.
|
|
131
131
|
|
|
132
|
+
## AI coding agent setup (`muscat-pk mcp`)
|
|
133
|
+
|
|
134
|
+
`muscat-pk mcp` runs an MCP server (stdio) that gives an AI coding agent grounded info about `rb-components`, design tokens, and the Python `rainbow-rb-sdk` — actual prop types, real Storybook examples, and live-introspected SDK signatures/docstrings, instead of the agent guessing from training data. Use it while working inside a plugin project scaffolded by `muscat-pk new`.
|
|
135
|
+
|
|
136
|
+
### Claude Code
|
|
137
|
+
|
|
138
|
+
Claude Code reads a project-scoped `.mcp.json` at the plugin's root:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"mcpServers": {
|
|
143
|
+
"muscat": {
|
|
144
|
+
"command": "muscat-pk",
|
|
145
|
+
"args": ["mcp"]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Codex
|
|
152
|
+
|
|
153
|
+
Codex has no project-scoped config file — it reads a single, per-user `~/.codex/config.toml`. Add:
|
|
154
|
+
|
|
155
|
+
```toml
|
|
156
|
+
[mcp_servers.muscat]
|
|
157
|
+
command = "muscat-pk"
|
|
158
|
+
args = ["mcp"]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
You don't run `muscat-pk mcp` yourself either way — the agent spawns and manages it automatically once it's registered.
|
|
162
|
+
|
|
132
163
|
## Package roles
|
|
133
164
|
|
|
134
165
|
| Package | Role |
|
|
Binary file
|
package/cli/src/dev.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// muscat-pk dev — run the Socket.IO bridge (Docker), dev:ui, and dev:back together.
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
+
import { connect as netConnect } from "node:net";
|
|
4
5
|
import { release } from "node:os";
|
|
5
6
|
import { resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { runDevUi } from "./devUi.mjs";
|
|
7
9
|
import { runDevBack, stopDevBackContainers } from "./devBack.mjs";
|
|
8
10
|
import { startBridge, stopBridge, BRIDGE_CONTAINER, BRIDGE_PORT } from "./bridge.mjs";
|
|
@@ -17,64 +19,66 @@ import {
|
|
|
17
19
|
promptRobotConnection,
|
|
18
20
|
venvPython,
|
|
19
21
|
} from "./util.mjs";
|
|
20
|
-
import {
|
|
22
|
+
import { ensureRemoteZenohEnv, isRemoteRobot } from "./zenohDev.mjs";
|
|
21
23
|
|
|
22
|
-
const
|
|
24
|
+
const HOST_AGENT_PATH = fileURLToPath(new URL("./host_agent.py", import.meta.url));
|
|
23
25
|
const isWsl = () => process.platform === "linux" && /microsoft|wsl/i.test(release());
|
|
24
26
|
const canAutoStartHostAgent = () => process.platform === "darwin" || isWsl();
|
|
25
27
|
|
|
26
28
|
function waitForHostAgent(port, { timeoutMs = 30_000 } = {}) {
|
|
27
29
|
const deadline = Date.now() + timeoutMs;
|
|
28
|
-
const url = `http://127.0.0.1:${port}/health`;
|
|
29
30
|
return new Promise((resolve, reject) => {
|
|
30
|
-
const probe =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
setTimeout(probe, 300);
|
|
31
|
+
const probe = () => {
|
|
32
|
+
const sock = netConnect(port, "127.0.0.1");
|
|
33
|
+
sock.once("connect", () => { sock.destroy(); resolve(true); });
|
|
34
|
+
sock.once("error", () => {
|
|
35
|
+
sock.destroy();
|
|
36
|
+
if (Date.now() >= deadline) {
|
|
37
|
+
reject(new Error(`host SDK agent did not open port ${port} in time`));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setTimeout(probe, 300);
|
|
41
|
+
});
|
|
42
42
|
};
|
|
43
43
|
probe();
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
const DEFAULT_PROXY_PORT = 47100;
|
|
48
|
+
|
|
49
|
+
// Start the host-side Zenoh byte-proxy (host_agent.py) for a remote robot —
|
|
50
|
+
// see host_agent.py for why a Docker Desktop container can't join the
|
|
51
|
+
// robot's Zenoh mesh itself. Sets MUSCAT_PROXY_ENDPOINT for dev:back.
|
|
52
|
+
// Returns the child process, or null if not applicable / disabled.
|
|
47
53
|
async function maybeStartHostAgent(root, { robotIp, robotToken }) {
|
|
48
|
-
if (process.env.
|
|
49
|
-
log.step(`host
|
|
54
|
+
if (process.env.MUSCAT_PROXY_ENDPOINT) {
|
|
55
|
+
log.step(`Zenoh via host byte-proxy → ${process.env.MUSCAT_PROXY_ENDPOINT}`);
|
|
50
56
|
return null;
|
|
51
57
|
}
|
|
52
58
|
if (process.env.MUSCAT_HOST_AGENT_DISABLE === "1") return null;
|
|
53
59
|
if (!canAutoStartHostAgent() || !isRemoteRobot(robotIp)) return null;
|
|
54
60
|
|
|
55
|
-
const agentPath =
|
|
61
|
+
const agentPath = process.env.MUSCAT_HOST_AGENT_SCRIPT || HOST_AGENT_PATH;
|
|
56
62
|
if (!existsSync(agentPath)) return null;
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
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");
|
|
64
|
+
if (!ensureIdeVenv(root)) {
|
|
65
|
+
log.warn("host Zenoh byte-proxy skipped: could not prepare .venv");
|
|
68
66
|
return null;
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
// advertiseHost/zenohPort = the host_agent's OWN robot-facing Zenoh session
|
|
70
|
+
// (host LAN IP + port the robot reverse-dials into). The container never
|
|
71
|
+
// sees or needs these — it only talks to the proxyPort below.
|
|
72
|
+
const zenoh = await ensureRemoteZenohEnv(robotIp, { log });
|
|
73
|
+
const advertiseHost = zenoh?.advertiseHost || process.env.MUSCAT_ZENOH_ADVERTISE_HOST;
|
|
74
|
+
const zenohPort = zenoh?.zenohPort || Number(process.env.MUSCAT_ZENOH_PORT);
|
|
75
|
+
if (!advertiseHost || !zenohPort) {
|
|
76
|
+
log.warn("host Zenoh byte-proxy skipped: could not determine host LAN IP / port");
|
|
74
77
|
return null;
|
|
75
78
|
}
|
|
79
|
+
const proxyPort = Number(process.env.MUSCAT_PROXY_PORT) || DEFAULT_PROXY_PORT;
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
log.step("starting host Zenoh byte-proxy (Docker Desktop query-routing workaround)...");
|
|
78
82
|
const py = venvPython(resolve(root, ".venv"));
|
|
79
83
|
const child = spawn(py, [agentPath], {
|
|
80
84
|
cwd: root,
|
|
@@ -83,21 +87,27 @@ async function maybeStartHostAgent(root, { robotIp, robotToken }) {
|
|
|
83
87
|
...process.env,
|
|
84
88
|
MUSCAT_ROBOT_HOST: robotIp,
|
|
85
89
|
MUSCAT_ROBOT_TOKEN: robotToken,
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
MUSCAT_ZENOH_ADVERTISE_HOST: advertiseHost,
|
|
91
|
+
MUSCAT_ZENOH_PORT: String(zenohPort),
|
|
92
|
+
MUSCAT_PROXY_PORT: String(proxyPort),
|
|
88
93
|
},
|
|
89
94
|
});
|
|
90
95
|
|
|
91
96
|
try {
|
|
92
|
-
await waitForHostAgent(
|
|
97
|
+
await waitForHostAgent(proxyPort);
|
|
93
98
|
} catch (e) {
|
|
94
99
|
child.kill();
|
|
95
100
|
throw e;
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
// advertiseHost (not host.docker.internal): a Docker Desktop container run
|
|
104
|
+
// under `dev` may share the bridge container's netns (--network container:),
|
|
105
|
+
// which has no host-gateway alias configured — the host's real LAN IP
|
|
106
|
+
// always resolves regardless of netns mode.
|
|
107
|
+
const proxyEndpoint = `${advertiseHost}:${proxyPort}`;
|
|
108
|
+
process.env.MUSCAT_PROXY_ENDPOINT = proxyEndpoint;
|
|
109
|
+
process.env.MUSCAT_PROXY_PORT = String(proxyPort);
|
|
110
|
+
log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
|
|
101
111
|
return child;
|
|
102
112
|
}
|
|
103
113
|
|
|
@@ -115,18 +125,14 @@ export async function runDev(args = []) {
|
|
|
115
125
|
|
|
116
126
|
const remoteRobot = isRemoteRobot(robotIp);
|
|
117
127
|
const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
|
|
118
|
-
const
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
if (
|
|
122
|
-
log.step(`host SDK agent → ${hostAgentUrl}`);
|
|
123
|
-
} else if (!hostAgentUrl) {
|
|
124
|
-
await ensureRemoteZenohEnv(robotIp, { log });
|
|
125
|
-
}
|
|
128
|
+
const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT || "";
|
|
129
|
+
// No proxy (Linux, disabled, ...): fall back to the container reverse-dialing
|
|
130
|
+
// the robot itself — startBridge() then publishes the zenoh port for it.
|
|
131
|
+
if (!proxyEndpoint) await ensureRemoteZenohEnv(robotIp, { log });
|
|
126
132
|
|
|
127
133
|
log.step("starting Socket.IO bridge (Docker) + dev:ui + dev:back");
|
|
128
134
|
const bridgeStarted = await startBridge({
|
|
129
|
-
zenohPort: remoteRobot && !
|
|
135
|
+
zenohPort: remoteRobot && !proxyEndpoint ? Number(process.env.MUSCAT_ZENOH_PORT) : null,
|
|
130
136
|
});
|
|
131
137
|
|
|
132
138
|
if (!bridgeStarted) {
|
package/cli/src/devBack.mjs
CHANGED
|
@@ -87,20 +87,23 @@ 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
|
-
|
|
90
|
+
// See host_agent.py: a Docker Desktop container can't join the robot's
|
|
91
|
+
// Zenoh mesh, so it speaks a byte protocol to the host-side proxy instead.
|
|
92
|
+
const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT ?? "";
|
|
91
93
|
// Share the bridge container's network namespace so localhost:10000 is the
|
|
92
94
|
// bridge (set by `muscat-pk dev`). Standalone dev:back has no bridge to join.
|
|
93
95
|
const bridgeContainer = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
|
|
94
96
|
const netFlags = bridgeContainer ? ["--network", `container:${bridgeContainer}`] : [];
|
|
95
|
-
|
|
97
|
+
// Only the container's OWN reverse-dial path needs to publish a listen port.
|
|
98
|
+
// With the byte-proxy, the container just connects out to host.docker.internal
|
|
99
|
+
// — nothing to publish.
|
|
100
|
+
const standaloneZenohPortFlags =
|
|
101
|
+
!bridgeContainer && !proxyEndpoint && zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : [];
|
|
96
102
|
const hostGatewayFlags = bridgeContainer
|
|
97
103
|
? []
|
|
98
104
|
: ["--add-host", "host.docker.internal:host-gateway"];
|
|
99
|
-
// Without the bridge's netns,
|
|
100
|
-
//
|
|
101
|
-
// it can never see a gateway already running on the host (e.g. the user's
|
|
102
|
-
// own robot stack occupying port 10000, the reason the bridge start above
|
|
103
|
-
// was skipped in the first place). host.docker.internal reaches the host.
|
|
105
|
+
// Without the bridge's netns, 127.0.0.1 (the SDK's gateway default) is the
|
|
106
|
+
// container itself, not the host — host.docker.internal reaches the host.
|
|
104
107
|
const gatewayFlags = bridgeContainer
|
|
105
108
|
? []
|
|
106
109
|
: ["-e", "RB_PLUGIN_GATEWAY_URL=http://host.docker.internal:10000"];
|
|
@@ -113,9 +116,13 @@ async function runPython(root, portRanges) {
|
|
|
113
116
|
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
114
117
|
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
115
118
|
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
// Byte-proxy mode: the container has no Zenoh session of its own — the
|
|
120
|
+
// entry script's shim speaks the raw query/put/subscribe protocol to the
|
|
121
|
+
// host_agent process at this address instead.
|
|
122
|
+
...(proxyEndpoint ? ["-e", `MUSCAT_PROXY_ENDPOINT=${proxyEndpoint}`] : []),
|
|
123
|
+
// Fallback (no proxy): the container reverse-dials the robot itself.
|
|
124
|
+
...(!proxyEndpoint && zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
|
|
125
|
+
...(!proxyEndpoint && zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
|
|
119
126
|
...portEnvFlags(portRanges),
|
|
120
127
|
];
|
|
121
128
|
const docker = (args, opts = {}) =>
|
|
@@ -142,69 +149,196 @@ async function runPython(root, portRanges) {
|
|
|
142
149
|
" && { [ -f requirements.txt ] && pip install --break-system-packages --ignore-installed -q -r requirements.txt || true; }"]);
|
|
143
150
|
|
|
144
151
|
docker(["exec", container, "bash", "-lc", `cat > /tmp/muscat-pk-python-dev-entry.py <<'PY'
|
|
145
|
-
import json
|
|
146
152
|
import os
|
|
147
|
-
import urllib.request
|
|
148
|
-
|
|
149
|
-
def install_host_agent_sdk_patch(host_agent_url):
|
|
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
|
|
198
153
|
|
|
199
154
|
try:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
155
|
+
proxy_endpoint = os.getenv("MUSCAT_PROXY_ENDPOINT")
|
|
156
|
+
if proxy_endpoint:
|
|
157
|
+
# Byte-proxy mode: this container has no Zenoh session of its own.
|
|
158
|
+
# ZenohClient's C++ core is replaced with a shim that speaks a raw
|
|
159
|
+
# query/put/subscribe byte protocol to host_agent.py instead.
|
|
160
|
+
import json
|
|
161
|
+
import socket
|
|
162
|
+
import struct
|
|
163
|
+
import threading
|
|
164
|
+
|
|
165
|
+
_proxy_host, _proxy_port_s = proxy_endpoint.rsplit(":", 1)
|
|
166
|
+
_proxy_port = int(_proxy_port_s)
|
|
167
|
+
|
|
168
|
+
def _send_frame(conn, lock, obj):
|
|
169
|
+
body = json.dumps(obj).encode()
|
|
170
|
+
with lock:
|
|
171
|
+
conn.sendall(struct.pack(">I", len(body)) + body)
|
|
172
|
+
|
|
173
|
+
def _recv_frame(conn):
|
|
174
|
+
hdr = b""
|
|
175
|
+
while len(hdr) < 4:
|
|
176
|
+
chunk = conn.recv(4 - len(hdr))
|
|
177
|
+
if not chunk:
|
|
178
|
+
return None
|
|
179
|
+
hdr += chunk
|
|
180
|
+
n = struct.unpack(">I", hdr)[0]
|
|
181
|
+
buf = b""
|
|
182
|
+
while len(buf) < n:
|
|
183
|
+
chunk = conn.recv(n - len(buf))
|
|
184
|
+
if not chunk:
|
|
185
|
+
return None
|
|
186
|
+
buf += chunk
|
|
187
|
+
return json.loads(buf.decode())
|
|
188
|
+
|
|
189
|
+
class _ProxyRawClient:
|
|
190
|
+
"""Drop-in replacement for ZenohClient's C++ core, backed by the
|
|
191
|
+
host_agent byte-proxy instead of a local Zenoh session."""
|
|
192
|
+
|
|
193
|
+
sender = "RRS"
|
|
194
|
+
sender_id = ""
|
|
195
|
+
|
|
196
|
+
def __init__(self, host, port):
|
|
197
|
+
self._conn = socket.create_connection((host, port), timeout=10)
|
|
198
|
+
# create_connection() leaves the connect timeout applied to
|
|
199
|
+
# the socket permanently; the background read loop below does
|
|
200
|
+
# a long-lived blocking recv() for async "sample" pushes, so
|
|
201
|
+
# it must not time out on an idle connection.
|
|
202
|
+
self._conn.settimeout(None)
|
|
203
|
+
self._send_lock = threading.Lock()
|
|
204
|
+
self._req_id = 0
|
|
205
|
+
self._req_lock = threading.Lock()
|
|
206
|
+
self._pending = {}
|
|
207
|
+
self._subs = {} # sub_id -> callback
|
|
208
|
+
self._sub_by_keyexpr = {}
|
|
209
|
+
threading.Thread(target=self._read_loop, daemon=True).start()
|
|
210
|
+
|
|
211
|
+
def _next_id(self):
|
|
212
|
+
with self._req_lock:
|
|
213
|
+
self._req_id += 1
|
|
214
|
+
return self._req_id
|
|
215
|
+
|
|
216
|
+
def _read_loop(self):
|
|
217
|
+
while True:
|
|
218
|
+
frame = _recv_frame(self._conn)
|
|
219
|
+
if frame is None:
|
|
220
|
+
return
|
|
221
|
+
ftype = frame.get("type")
|
|
222
|
+
if ftype == "sample":
|
|
223
|
+
cb = self._sub_by_keyexpr.get(frame["keyexpr"])
|
|
224
|
+
if cb is not None:
|
|
225
|
+
try:
|
|
226
|
+
cb(frame["key"], bytes.fromhex(frame["payload_hex"]), frame.get("att", ""))
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
print(f"[muscat-pk] proxy subscriber callback error: {exc}", flush=True)
|
|
229
|
+
elif "id" in frame:
|
|
230
|
+
ev = self._pending.pop(frame["id"], None)
|
|
231
|
+
if ev is not None:
|
|
232
|
+
ev[1] = frame
|
|
233
|
+
ev[0].set()
|
|
234
|
+
|
|
235
|
+
def _request(self, obj, timeout=30):
|
|
236
|
+
req_id = self._next_id()
|
|
237
|
+
obj["id"] = req_id
|
|
238
|
+
ev = threading.Event()
|
|
239
|
+
slot = [ev, None]
|
|
240
|
+
self._pending[req_id] = slot
|
|
241
|
+
_send_frame(self._conn, self._send_lock, obj)
|
|
242
|
+
if not ev.wait(timeout):
|
|
243
|
+
self._pending.pop(req_id, None)
|
|
244
|
+
raise TimeoutError(f"proxy request timed out: {obj.get('type')} {obj.get('keyexpr')}")
|
|
245
|
+
return slot[1]
|
|
246
|
+
|
|
247
|
+
def query(self, keyexpr, payload, timeout_ms, target):
|
|
248
|
+
resp = self._request({
|
|
249
|
+
"type": "query", "keyexpr": keyexpr,
|
|
250
|
+
"payload_hex": payload.hex() if payload else None,
|
|
251
|
+
"timeout_ms": timeout_ms, "target": target,
|
|
252
|
+
}, timeout=(timeout_ms / 1000) + 5)
|
|
253
|
+
if not resp.get("ok"):
|
|
254
|
+
raise RuntimeError(resp.get("error", "proxy query failed"))
|
|
255
|
+
return [
|
|
256
|
+
(r["key"], bytes.fromhex(r["payload_hex"]), r["att"], r["encoding"], r["is_error"])
|
|
257
|
+
for r in resp["replies"]
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
def query_all(self, keyexpr, payload, timeout_ms, target):
|
|
261
|
+
return self.query(keyexpr, payload, timeout_ms, target)
|
|
262
|
+
|
|
263
|
+
def publish(self, topic, payload, att, congestion_control, priority, express):
|
|
264
|
+
resp = self._request({
|
|
265
|
+
"type": "put", "keyexpr": topic,
|
|
266
|
+
"payload_hex": bytes(payload).hex() if payload else None,
|
|
267
|
+
})
|
|
268
|
+
if not resp.get("ok"):
|
|
269
|
+
raise RuntimeError(resp.get("error", "proxy put failed"))
|
|
270
|
+
|
|
271
|
+
def has_subscriber(self, topic):
|
|
272
|
+
return topic in self._sub_by_keyexpr
|
|
273
|
+
|
|
274
|
+
def declare_subscriber(self, topic, callback, opts=None):
|
|
275
|
+
resp = self._request({"type": "subscribe", "keyexpr": topic})
|
|
276
|
+
if not resp.get("ok"):
|
|
277
|
+
raise RuntimeError(resp.get("error", "proxy subscribe failed"))
|
|
278
|
+
sub_id = self._next_id()
|
|
279
|
+
self._subs[sub_id] = topic
|
|
280
|
+
self._sub_by_keyexpr[topic] = callback
|
|
281
|
+
return sub_id
|
|
282
|
+
|
|
283
|
+
def undeclare_subscriber(self, sub_id):
|
|
284
|
+
topic = self._subs.pop(sub_id, None)
|
|
285
|
+
if topic is None:
|
|
286
|
+
return
|
|
287
|
+
self._sub_by_keyexpr.pop(topic, None)
|
|
288
|
+
self._request({"type": "unsubscribe", "keyexpr": topic})
|
|
289
|
+
|
|
290
|
+
def unsubscribe_topic(self, topic):
|
|
291
|
+
for sub_id, t in list(self._subs.items()):
|
|
292
|
+
if t == topic:
|
|
293
|
+
self.undeclare_subscriber(sub_id)
|
|
294
|
+
|
|
295
|
+
def unsubscribe_all(self):
|
|
296
|
+
for sub_id in list(self._subs):
|
|
297
|
+
self.undeclare_subscriber(sub_id)
|
|
298
|
+
|
|
299
|
+
def get_sub_stats(self, sub_id):
|
|
300
|
+
return {}
|
|
301
|
+
|
|
302
|
+
def declare_queryable(self, keyexpr, handler):
|
|
303
|
+
raise NotImplementedError("byte-proxy mode does not support declare_queryable")
|
|
304
|
+
|
|
305
|
+
def undeclare_queryable_by_topic(self, keyexpr):
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
# ZenohClient is a process-wide singleton whose __init__ eagerly
|
|
309
|
+
# calls connect() on first construction. rainbow.rb_sdk.base
|
|
310
|
+
# transitively imports rainbow.rb_log.log, which constructs one at
|
|
311
|
+
# module level — so this patch must land before RBBaseSDK is
|
|
312
|
+
# imported, or that eager construction wins with a real session.
|
|
313
|
+
from rainbow.rb_zenoh.client import ZenohClient
|
|
314
|
+
|
|
315
|
+
def _proxy_connect(self, *, config_pairs=None, sender=None, max_attempts=8, initial_delay_s=0.5, max_delay_s=3.0):
|
|
316
|
+
if self._client is not None:
|
|
317
|
+
return
|
|
318
|
+
if sender is not None:
|
|
319
|
+
self._sender = sender
|
|
320
|
+
print(f"[muscat-pk] zenoh via host byte-proxy → {proxy_endpoint} (no local Zenoh session)", flush=True)
|
|
321
|
+
self._client = _ProxyRawClient(_proxy_host, _proxy_port)
|
|
322
|
+
|
|
323
|
+
ZenohClient.connect = _proxy_connect
|
|
203
324
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
325
|
+
from rainbow.rb_sdk.base import RBBaseSDK
|
|
326
|
+
|
|
327
|
+
async def _sdk_connect_via_proxy(self, *args, **kwargs):
|
|
328
|
+
# No HTTP registration with common — host_agent already handles
|
|
329
|
+
# that. Just make sure the (proxy-backed) shared ZenohClient exists.
|
|
330
|
+
self._ensure_zenoh_client_alive()
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
async def _sdk_disconnect_via_proxy(self, *args, **kwargs):
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
RBBaseSDK.connect = _sdk_connect_via_proxy
|
|
337
|
+
RBBaseSDK.disconnect = _sdk_disconnect_via_proxy
|
|
207
338
|
else:
|
|
339
|
+
# Fallback: no host byte-proxy — the container reverse-dials the robot itself.
|
|
340
|
+
advertise_host = os.getenv("MUSCAT_ZENOH_ADVERTISE_HOST")
|
|
341
|
+
zenoh_port = os.getenv("MUSCAT_ZENOH_PORT")
|
|
208
342
|
from rainbow.rb_sdk.base import RBBaseSDK
|
|
209
343
|
if advertise_host:
|
|
210
344
|
RBBaseSDK._detect_local_ip = staticmethod(lambda host: advertise_host)
|
|
@@ -224,8 +358,8 @@ runpy.run_path("/plugin/py/main.py", run_name="__main__")
|
|
|
224
358
|
PY`]);
|
|
225
359
|
|
|
226
360
|
if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
|
|
227
|
-
if (
|
|
228
|
-
if (
|
|
361
|
+
if (proxyEndpoint) log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
|
|
362
|
+
else if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
|
|
229
363
|
log.step("watchfiles → py/main.py (reload on any .py change in py/)");
|
|
230
364
|
const child = spawn(
|
|
231
365
|
"docker",
|
|
@@ -445,9 +579,9 @@ export async function runDevBack(args = []) {
|
|
|
445
579
|
if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
|
|
446
580
|
// Standalone `dev:back` against a remote robot needs the Zenoh reverse-path
|
|
447
581
|
// advertise host/port too (idempotent — `muscat-pk dev` already set these).
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
if (!process.env.
|
|
582
|
+
// Byte-proxy mode (a host-side host_agent set MUSCAT_PROXY_ENDPOINT) makes
|
|
583
|
+
// the container speak the proxy protocol instead, so it needs no reverse-dial.
|
|
584
|
+
if (!process.env.MUSCAT_PROXY_ENDPOINT) await ensureRemoteZenohEnv(robotIp, { log });
|
|
451
585
|
const type = manifestRuntime(root);
|
|
452
586
|
if (type === "ros2") return await runRos2(root, portRanges);
|
|
453
587
|
if (type === "cpp") return await runCpp(root, portRanges);
|