Files
planet/frontend/public/earth/js/motion-agent-provider.js
linkong 899e3bce43
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
release: bump version to 0.71.0
2026-06-11 16:47:24 +08:00

142 lines
3.7 KiB
JavaScript

const DEFAULT_RECONNECT_MS = 1800;
export const DEFAULT_AGENT_URL = "ws://127.0.0.1:8765/ws/gestures";
export function createMotionAgentProvider(options = {}) {
const {
url = DEFAULT_AGENT_URL,
reconnectMs = DEFAULT_RECONNECT_MS,
WebSocketCtor = typeof WebSocket !== "undefined" ? WebSocket : null,
onMessage = () => {},
onState = () => {},
onStatus = () => {},
debugSkeleton = false,
enabledGestures = [],
} = options;
let socket = null;
let reconnectTimer = null;
let disposed = false;
let connected = false;
let requestSeq = 0;
function emitState(detail = {}) {
onState({
provider: "motion_agent",
connected,
url,
...detail,
});
}
function clearReconnect() {
if (!reconnectTimer) return;
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
function scheduleReconnect() {
if (disposed || reconnectTimer) return;
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connect();
}, reconnectMs);
}
function closeSocket() {
if (!socket) return;
const current = socket;
socket = null;
current.onopen = null;
current.onmessage = null;
current.onerror = null;
current.onclose = null;
try {
current.close();
} catch (_error) {
// Browser WebSocket close can throw during teardown in older engines.
}
}
function sendCommand(command, payload = {}, requestId = null) {
if (!socket || !connected || socket.readyState !== 1) {
return {
ok: false,
error: "motion_agent_not_connected",
requestId: requestId || null,
};
}
const nextRequestId = requestId || `earth-motion-${Date.now()}-${++requestSeq}`;
socket.send(JSON.stringify({
type: "command",
command,
request_id: nextRequestId,
payload,
}));
return { ok: true, requestId: nextRequestId };
}
function connect() {
if (disposed || socket) return;
if (!WebSocketCtor) {
emitState({ error: "websocket_unavailable", message: "当前浏览器不支持 WebSocket" });
return;
}
try {
socket = new WebSocketCtor(url);
} catch (_error) {
emitState({ error: "socket_create_failed", message: "无法创建 Motion Agent 连接" });
scheduleReconnect();
return;
}
socket.onopen = () => {
connected = true;
emitState({ connected: true });
sendCommand("set_debug_options", { skeleton: Boolean(debugSkeleton) }, "earth-motion-debug-on-connect");
if (Array.isArray(enabledGestures) && enabledGestures.length > 0) {
sendCommand(
"set_enabled_gestures",
{ gestures: enabledGestures },
"earth-motion-enabled-gestures-on-connect",
);
}
sendCommand("set_armed", { armed: true }, "earth-motion-armed-on-connect");
onStatus("动捕 Agent 已连接", "info");
};
socket.onmessage = (rawMessage) => onMessage(rawMessage?.data ?? rawMessage);
socket.onerror = () => {
emitState({ connected: false, error: "socket_error", message: "Motion Agent 连接异常" });
};
socket.onclose = () => {
connected = false;
socket = null;
emitState({ connected: false, message: "Motion Agent 未连接" });
scheduleReconnect();
};
}
return {
provider: "motion_agent",
start() {
if (disposed) return false;
connect();
return true;
},
stop() {
disposed = true;
clearReconnect();
closeSocket();
connected = false;
emitState({ connected: false });
},
isConnected() {
return connected;
},
sendCommand(command, payload = {}, requestId = null) {
return sendCommand(command, payload, requestId);
},
};
}