@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.6 → 0.1.2-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/bin/muscat-pk.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { runDev } from "../src/dev.mjs";
|
|
|
10
10
|
import { runUpdate } from "../src/update.mjs";
|
|
11
11
|
import { runSdk } from "../src/sdk.mjs";
|
|
12
12
|
import { runType } from "../src/type.mjs";
|
|
13
|
+
import { runPreview } from "../src/preview.mjs";
|
|
13
14
|
|
|
14
15
|
const [, , cmd, ...args] = process.argv;
|
|
15
16
|
|
|
@@ -25,6 +26,7 @@ Commands:
|
|
|
25
26
|
dev Run dev:ui and dev:back together
|
|
26
27
|
dev:ui Preview the plugin in a mini-Muscat (preview-host)
|
|
27
28
|
dev:back Run the plugin back end (python: watchfiles reload / ros2: Docker)
|
|
29
|
+
preview Preview against a real Muscat server (prompts for robot IP)
|
|
28
30
|
update Update @rainbow-robotics/muscat-plugin-kit and @rainbow-robotics/plugin-ui
|
|
29
31
|
sdk <list|update [version]> Manage the python rainbow-rb-sdk version
|
|
30
32
|
|
|
@@ -47,6 +49,8 @@ async function main() {
|
|
|
47
49
|
return await runDevUi(args);
|
|
48
50
|
case "dev:back":
|
|
49
51
|
return await runDevBack(args);
|
|
52
|
+
case "preview":
|
|
53
|
+
return await runPreview(args);
|
|
50
54
|
case "update":
|
|
51
55
|
return await runUpdate(args);
|
|
52
56
|
case "sdk":
|
package/cli/src/devUi.mjs
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// muscat-pk preview — build the plugin and open preview-host connected to a
|
|
2
|
+
// real Muscat server (robot or local). No local Socket.IO server is started.
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { buildPlugin } from "./build.mjs";
|
|
8
|
+
import { findPreviewHostRoot } from "./devUi.mjs";
|
|
9
|
+
import { checkKitUpdate, log, parseArgs } from "./util.mjs";
|
|
10
|
+
|
|
11
|
+
function prompt(question, defaultValue) {
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise((res) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
res(answer.trim() || defaultValue);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runPreview(args = []) {
|
|
22
|
+
const opts = parseArgs(args, { booleans: ["help"] });
|
|
23
|
+
|
|
24
|
+
if (opts.help) {
|
|
25
|
+
console.log(
|
|
26
|
+
[
|
|
27
|
+
"",
|
|
28
|
+
"Usage: muscat-pk preview [options]",
|
|
29
|
+
"",
|
|
30
|
+
" 빌드된 플러그인을 실제 Muscat 서버에 연결해 미리봅니다.",
|
|
31
|
+
" Socket.IO 서버를 직접 띄우지 않습니다.",
|
|
32
|
+
"",
|
|
33
|
+
"Options:",
|
|
34
|
+
" --host <ip> Muscat 서버 IP 또는 hostname (default: localhost)",
|
|
35
|
+
" --port <n> preview-host 로컬 포트 (default: 4180)",
|
|
36
|
+
" --ws-port <n> Muscat Socket.IO 포트 (default: 10000)",
|
|
37
|
+
"",
|
|
38
|
+
].join("\n"),
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await checkKitUpdate();
|
|
44
|
+
|
|
45
|
+
// ── 로봇 IP 결정 ────────────────────────────────────────────────────────────
|
|
46
|
+
let robotHost = opts.host ?? "";
|
|
47
|
+
if (!robotHost) {
|
|
48
|
+
robotHost = await prompt(
|
|
49
|
+
"Muscat 서버 IP 또는 hostname [localhost]: ",
|
|
50
|
+
"localhost",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const wsPort = opts["ws-port"] ?? "10000";
|
|
54
|
+
const socketUrl = `ws://${robotHost}:${wsPort}`;
|
|
55
|
+
|
|
56
|
+
const previewPort = opts.port ?? "4180";
|
|
57
|
+
|
|
58
|
+
log.step(`Muscat 서버: ${socketUrl}`);
|
|
59
|
+
|
|
60
|
+
// ── 플러그인 빌드 ────────────────────────────────────────────────────────────
|
|
61
|
+
const { distDir } = await buildPlugin();
|
|
62
|
+
|
|
63
|
+
// ── preview-host 실행 ────────────────────────────────────────────────────────
|
|
64
|
+
const previewRoot = findPreviewHostRoot();
|
|
65
|
+
const serveEntry = resolve(previewRoot, "serve.mjs");
|
|
66
|
+
|
|
67
|
+
if (!existsSync(resolve(previewRoot, "dist", "index.html"))) {
|
|
68
|
+
log.step("preview-host 번들 빌드 중 (최초 1회)…");
|
|
69
|
+
await new Promise((res, rej) => {
|
|
70
|
+
const p = spawn(process.execPath, [resolve(previewRoot, "build.mjs")], {
|
|
71
|
+
stdio: "inherit",
|
|
72
|
+
});
|
|
73
|
+
p.on("exit", (c) => (c === 0 ? res() : rej(new Error("preview-host build failed"))));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
log.step(`preview-host 시작 → http://localhost:${previewPort}`);
|
|
78
|
+
log.step(`Socket.IO URL: ${socketUrl}`);
|
|
79
|
+
|
|
80
|
+
const child = spawn(
|
|
81
|
+
process.execPath,
|
|
82
|
+
[
|
|
83
|
+
serveEntry,
|
|
84
|
+
"--plugin", distDir,
|
|
85
|
+
"--port", String(previewPort),
|
|
86
|
+
"--socket-url", socketUrl,
|
|
87
|
+
],
|
|
88
|
+
{ stdio: "inherit" },
|
|
89
|
+
);
|
|
90
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
91
|
+
}
|
|
@@ -1,24 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Muscat plugin backend — __PLUGIN_NAME__
|
|
3
|
+
|
|
4
|
+
플러그인 UI(브라우저)와 Socket.IO로 통신한다.
|
|
5
|
+
`muscat-pk dev:back` 또는 `python main.py`로 실행.
|
|
6
|
+
|
|
7
|
+
토픽 자동 네임스페이스:
|
|
8
|
+
"sensor/data" → plugin/__PLUGIN_ID__/sensor/data (자동)
|
|
9
|
+
"/broadcast/x" → broadcast/x (앞 / = 절대경로)
|
|
10
|
+
"""
|
|
11
|
+
|
|
1
12
|
import asyncio
|
|
13
|
+
import socketio
|
|
14
|
+
from aiohttp import web
|
|
15
|
+
|
|
16
|
+
sio = socketio.AsyncServer(async_mode="aiohttp", cors_allowed_origins="*")
|
|
17
|
+
app = web.Application()
|
|
18
|
+
sio.attach(app)
|
|
19
|
+
|
|
20
|
+
PLUGIN_ID = "__PLUGIN_ID__"
|
|
21
|
+
_PREFIX = f"plugin/{PLUGIN_ID}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _topic(relative: str) -> str:
|
|
25
|
+
"""상대 토픽 → 전체 네임스페이스 토픽."""
|
|
26
|
+
return relative if relative.startswith("/") else f"{_PREFIX}/{relative}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── 구독·쿼리어블 핸들러 저장소 ────────────────────────────────────────────────
|
|
30
|
+
_sub_handlers: dict[str, list] = {}
|
|
31
|
+
_queryable_handlers: dict[str, object] = {}
|
|
32
|
+
_serve_pending: dict[str, asyncio.Future] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Socket.IO 이벤트 ───────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
@sio.event
|
|
38
|
+
async def sub(sid, data):
|
|
39
|
+
"""브라우저가 토픽을 구독."""
|
|
40
|
+
await sio.enter_room(sid, f"t:{data['topic']}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@sio.event
|
|
44
|
+
async def unsub(sid, data):
|
|
45
|
+
await sio.leave_room(sid, f"t:{data['topic']}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@sio.event
|
|
49
|
+
async def pub(sid, data):
|
|
50
|
+
"""브라우저가 발행 → Python 구독 핸들러로 전달."""
|
|
51
|
+
topic = data.get("topic", "")
|
|
52
|
+
payload = data.get("payload")
|
|
53
|
+
for handler in _sub_handlers.get(topic, []):
|
|
54
|
+
asyncio.create_task(handler(payload))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@sio.event
|
|
58
|
+
async def action(sid, data):
|
|
59
|
+
"""브라우저 rb.action() 호출 → Python queryable 핸들러 → ack 반환."""
|
|
60
|
+
topic = data.get("topic", "")
|
|
61
|
+
handler = _queryable_handlers.get(topic)
|
|
62
|
+
if handler:
|
|
63
|
+
return await handler(data.get("payload"))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@sio.event
|
|
67
|
+
async def serve_reply(sid, data):
|
|
68
|
+
"""브라우저가 serve_call에 응답."""
|
|
69
|
+
fut = _serve_pending.pop(data.get("id", ""), None)
|
|
70
|
+
if fut and not fut.done():
|
|
71
|
+
fut.set_result(data.get("payload"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── 헬퍼 데코레이터 ────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
def subscribe(relative_topic: str):
|
|
77
|
+
"""브라우저 → Python 메시지 구독 핸들러 등록."""
|
|
78
|
+
def decorator(fn):
|
|
79
|
+
_sub_handlers.setdefault(_topic(relative_topic), []).append(fn)
|
|
80
|
+
return fn
|
|
81
|
+
return decorator
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def queryable(relative_topic: str):
|
|
85
|
+
"""브라우저 rb.action() 호출을 처리할 핸들러 등록."""
|
|
86
|
+
def decorator(fn):
|
|
87
|
+
_queryable_handlers[_topic(relative_topic)] = fn
|
|
88
|
+
return fn
|
|
89
|
+
return decorator
|
|
2
90
|
|
|
3
|
-
from rainbow_rb_zenoh import ZenohRouter
|
|
4
91
|
|
|
5
|
-
|
|
92
|
+
async def publish(relative_topic: str, payload) -> None:
|
|
93
|
+
"""Python → 브라우저 구독자에게 메시지 발행."""
|
|
94
|
+
full = _topic(relative_topic)
|
|
95
|
+
await sio.emit("msg", {"topic": full, "payload": payload}, room=f"t:{full}")
|
|
6
96
|
|
|
7
97
|
|
|
8
|
-
|
|
9
|
-
async def on_sensor(*, dict_payload, **_):
|
|
10
|
-
print(f"[__PLUGIN_NAME__] received: {dict_payload}")
|
|
98
|
+
# ── 플러그인 로직 ──────────────────────────────────────────────────────────────
|
|
11
99
|
|
|
100
|
+
@subscribe("sensor/data")
|
|
101
|
+
async def on_sensor_data(payload):
|
|
102
|
+
print(f"[__PLUGIN_NAME__] sensor/data: {payload}")
|
|
12
103
|
|
|
13
|
-
@router.queryable("get-status")
|
|
14
|
-
async def get_status(req=None, **_):
|
|
15
|
-
return {"ok": True}
|
|
16
104
|
|
|
105
|
+
@queryable("get-status")
|
|
106
|
+
async def get_status(payload=None):
|
|
107
|
+
return {"ok": True, "plugin": "__PLUGIN_NAME__"}
|
|
17
108
|
|
|
18
|
-
async def main():
|
|
19
|
-
await router.startup()
|
|
20
|
-
await asyncio.Event().wait()
|
|
21
109
|
|
|
110
|
+
# ── 진입점 ─────────────────────────────────────────────────────────────────────
|
|
22
111
|
|
|
23
112
|
if __name__ == "__main__":
|
|
24
|
-
|
|
113
|
+
print("[__PLUGIN_NAME__] Socket.IO server starting on :10000")
|
|
114
|
+
web.run_app(app, host="0.0.0.0", port=10000)
|
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.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@types/react-dom": "^19.0.4"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@rainbow-robotics/preview-host": "
|
|
34
|
+
"@rainbow-robotics/preview-host": "0.1.2-dev.8",
|
|
35
35
|
"esbuild": "^0.25.0"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|