// 全局变量
let availableInterfaces = [];
let interfaceStatus = {};
let handConfigs = {}; // 存储每个手的配置
const handTypeIds = {
left: 0x28, // HAND_TYPE_LEFT
right: 0x27, // HAND_TYPE_RIGHT
};
// 主要控制模块
const LinkerHandController = {
// 常量定义
DEFAULTS: {
FINGER: {
OPEN: 64, // 完全张开值
CLOSED: 192, // 完全闭合值
NEUTRAL: 128, // 中间值
},
PALM: {
NEUTRAL: 128, // 中间值
LEFT: 48, // 左侧
RIGHT: 208, // 右侧
},
ANIMATION: {
DEFAULT_SPEED: 500, // 默认动画速度
},
},
// 预设姿势配置
PRESETS: {
FIST: [64, 64, 64, 64, 64, 64], // 握拳
OPEN: [192, 192, 192, 192, 192, 192], // 张开
THUMBSUP: [255, 255, 0, 0, 0, 0], // 竖起大拇指
POINT: [0, 0, 255, 0, 0, 0], // 食指指点
YO: [255, 255, 255, 0, 0, 255], // Yo!
GUN: [255, 255, 255, 255, 0, 0], // PONG!
WAVE: [40, 60, 80, 100, 120, 140], // 波浪形
PALM_LEFT: [48, 48, 48, 48], // 掌部左移
PALM_RIGHT: [208, 208, 208, 208], // 掌部右移
PALM_NEUTRAL: [128, 128, 128, 128], // 掌部中立
PALM_GUN: [0, 0, 0, 128], // 掌部 GUN
PINCH: [114, 63, 136, 0, 0, 0], // 捏取姿势
PALM_PINCH: [255, 163, 255, 127],
OK: [124, 31, 132, 255, 255, 255],
PALM_OK: [255, 163, 255, 127],
BIG_FIST: [49, 32, 40, 36, 41, 46], // 大握拳
PALM_BIG_FIST: [255, 235, 128, 128], // 大握拳掌部
BIG_OPEN: [255, 255, 255, 255, 255, 255], // 大张开
PALM_BIG_OPEN: [128, 128, 128, 128], // 大张开掌部
YEAH: [0, 103, 255, 255, 0, 0], // Yeah!
PALM_YEAH: [255, 235, 128, 128], // Yeah! 掌部
// 数字手势预设
ONE: [0, 57, 255, 0, 0, 0],
PALM_ONE: [255, 109, 255, 118],
TWO: [0, 57, 255, 255, 0, 0],
PALM_TWO: [255, 109, 255, 118],
THREE: [0, 57, 255, 255, 255, 0],
PALM_THREE: [255, 109, 255, 118],
FOUR: [0, 57, 255, 255, 255, 255],
PALM_FOUR: [255, 109, 255, 118],
FIVE: [255, 255, 255, 255, 255, 255],
PALM_FIVE: [255, 109, 255, 118],
SIX: [255, 255, 0, 0, 0, 255],
PALM_SIX: [255, 255, 255, 255],
SEVEN: [110, 137, 130, 109, 0, 0],
PALM_SEVEN: [255, 200, 199, 76],
EIGHT: [216, 240, 255, 36, 41, 46],
PALM_EIGHT: [106, 200, 199, 76],
NINE: [0, 255, 159, 0, 0, 0],
PALM_NINE: [255, 38, 195, 51],
},
// 防抖函数
debounce: (func, delay) => {
let timer;
return () => {
clearTimeout(timer);
timer = setTimeout(func, delay);
};
},
// 初始化滑块显示与实时控制发送(带防抖)
initSliderDisplays: function () {
const fingerSliders = Array.from({ length: 6 }, (_, i) =>
document.getElementById(`finger${i}`),
);
const palmSliders = Array.from({ length: 4 }, (_, i) =>
document.getElementById(`palm${i}`),
);
const delayDefault = 30;
const updateFingerPose = this.debounce(() => {
const pose = this.getFingerPoseValues();
this.sendFingerPoseToAll(pose);
}, delayDefault);
const updatePalmPose = this.debounce(() => {
const pose = this.getPalmPoseValues();
this.sendPalmPoseToAll(pose);
}, delayDefault);
// 初始化手指滑块监听器
fingerSliders.forEach((slider, i) => {
slider.addEventListener("input", () => {
document.getElementById(`finger${i}-value`).textContent = slider.value;
updateFingerPose();
});
});
// 初始化掌部滑块监听器
palmSliders.forEach((slider, i) => {
slider.addEventListener("input", () => {
document.getElementById(`palm${i}-value`).textContent = slider.value;
updatePalmPose();
});
});
// 动画速度滑块更新
const animationSlider = document.getElementById("animation-speed");
animationSlider.addEventListener("input", function () {
document.getElementById("speed-value").textContent = this.value;
});
},
// 获取手指姿态值
getFingerPoseValues: () => {
const pose = [];
for (let i = 0; i < 6; i++) {
pose.push(Number.parseInt(document.getElementById(`finger${i}`).value));
}
return pose;
},
// 获取掌部姿态值
getPalmPoseValues: () => {
const pose = [];
for (let i = 0; i < 4; i++) {
pose.push(Number.parseInt(document.getElementById(`palm${i}`).value));
}
return pose;
},
// 设置手指滑块值
applyFingerPreset: (values) => {
if (!Array.isArray(values) || values.length !== 6) {
logMessage("error", "无效的手指预设值");
return;
}
// 设置滑块值
for (let i = 0; i < 6; i++) {
const slider = document.getElementById(`finger${i}`);
slider.value = values[i];
document.getElementById(`finger${i}-value`).textContent = values[i];
}
logMessage("info", "已应用手指预设姿势");
},
// 设置掌部滑块值
applyPalmPreset: (values) => {
if (!Array.isArray(values) || values.length !== 4) {
logMessage("error", "无效的掌部预设值");
return;
}
// 设置滑块值
for (let i = 0; i < 4; i++) {
const slider = document.getElementById(`palm${i}`);
slider.value = values[i];
document.getElementById(`palm${i}-value`).textContent = values[i];
}
logMessage("info", "已应用掌部预设姿势");
},
// 发送手指姿态到所有启用手部
sendFingerPoseToAll: (pose) => {
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
logMessage(
"info",
`发送手指姿态到 ${enabledHands.length} 个启用的手部:[${pose.join(
", ",
)}]`,
);
enabledHands.forEach(async (config) => {
await sendFingerPoseToHand(config, pose);
});
},
// 发送掌部姿态到所有启用手部
sendPalmPoseToAll: (pose) => {
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
logMessage(
"info",
`发送掌部姿态到 ${enabledHands.length} 个启用的手部:[${pose.join(
", ",
)}]`,
);
enabledHands.forEach(async (config) => {
await sendPalmPoseToHand(config, pose);
});
},
// 启动传感器数据轮询
startSensorDataPolling: function () {
// 立即获取一次数据
this.fetchSensorData();
// 设置定时获取
setInterval(() => {
this.fetchSensorData();
}, 2000); // 每 2 秒更新一次
},
// 获取传感器数据
fetchSensorData: function () {
fetch("/api/sensors")
.then((response) => response.json())
.then((data) => {
if (data.status === "success") {
this.updateSensorDisplay(data.data);
}
})
.catch((error) => {
console.error("获取传感器数据失败:", error);
});
},
// 更新传感器显示
updateSensorDisplay: function (data) {
const sensorDisplay = document.getElementById("sensor-data");
if (!sensorDisplay || !data) return;
// 创建进度条显示
let html = '
';
// 手指压力传感器
html += this.createSensorRow("拇指压力", data.thumb);
html += this.createSensorRow("食指压力", data.index);
html += this.createSensorRow("中指压力", data.middle);
html += this.createSensorRow("无名指压力", data.ring);
html += this.createSensorRow("小指压力", data.pinky);
html += "
";
// 更新最后更新时间
const lastUpdate = new Date(data.lastUpdate).toLocaleTimeString();
html += `最后更新:${lastUpdate}
`;
sensorDisplay.innerHTML = html;
},
// 创建传感器行
createSensorRow: (label, value) => {
if (value === undefined || value === null) value = 0;
return `
${label}
${value}%
`;
},
};
// 页面加载时初始化
document.addEventListener("DOMContentLoaded", () => {
initializeSystem();
setupEventListeners();
setupSliderEvents();
LinkerHandController.initSliderDisplays();
LinkerHandController.startSensorDataPolling();
startStatusUpdater();
});
// 初始化系统 - 添加更详细的错误处理和调试
async function initializeSystem() {
try {
logMessage("info", "开始初始化系统...");
// 步骤 1: 加载可用接口
logMessage("info", "步骤 1/3: 加载可用接口");
await loadAvailableInterfaces();
// 验证接口加载是否成功
if (!availableInterfaces || availableInterfaces.length === 0) {
throw new Error("未能获取到任何可用接口");
}
// 步骤 2: 生成手部配置
logMessage("info", "步骤 2/3: 生成手部配置");
generateHandConfigs();
// 验证手部配置是否成功
if (!handConfigs || Object.keys(handConfigs).length === 0) {
throw new Error("未能生成手部配置");
}
// 步骤 3: 检查接口状态
logMessage("info", "步骤 3/3: 检查接口状态");
await checkAllInterfaceStatus();
logMessage("success", "系统初始化完成");
} catch (error) {
logMessage("error", `系统初始化失败:${error.message}`);
console.error("InitializeSystem Error:", error);
// 尝试使用默认配置恢复
if (!availableInterfaces || availableInterfaces.length === 0) {
logMessage("info", "尝试使用默认配置恢复...");
availableInterfaces = ["can0", "can1", "vcan0", "vcan1"];
generateHandConfigs();
}
}
}
// 加载可用接口
async function loadAvailableInterfaces() {
try {
logMessage("info", "正在获取可用 CAN 接口...");
const response = await fetch("/api/interfaces");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.status === "success") {
availableInterfaces = data.data.availableInterfaces || [];
logMessage(
"success",
`获取到 ${
availableInterfaces.length
} 个可用接口:${availableInterfaces.join(", ")}`,
);
hideConnectionWarning();
} else {
throw new Error(data.error || "获取接口失败");
}
} catch (error) {
logMessage("error", `获取接口失败:${error.message}`);
showConnectionWarning();
// 设置默认值
availableInterfaces = ["can0", "can1", "vcan0", "vcan1"];
}
}
// 生成手部配置 - 添加调试和错误处理
function generateHandConfigs() {
const handsGrid = document.getElementById("hands-grid");
if (!handsGrid) {
console.error("Hands grid element not found");
logMessage("error", "无法找到手部配置容器");
return;
}
// 清空现有内容
handsGrid.innerHTML = "";
if (!availableInterfaces || availableInterfaces.length === 0) {
handsGrid.innerHTML =
'没有可用的 CAN 接口
';
logMessage("warning", "没有可用接口,无法生成手部配置");
return;
}
logMessage("info", `为 ${availableInterfaces.length} 个接口生成手部配置...`);
// 清空现有配置
handConfigs = {};
// 为每个接口创建配置项
availableInterfaces.forEach((iface, index) => {
const handId = `hand_${iface}`;
try {
// 创建默认配置
handConfigs[handId] = {
id: handId,
interface: iface,
handType: index % 2 === 0 ? "right" : "left", // 交替默认左右手
enabled: index < 2, // 默认启用前两个
status: "offline",
};
// 创建HTML元素
const handElement = createHandElement(handConfigs[handId]);
if (handElement) {
handsGrid.appendChild(handElement);
} else {
throw new Error("创建手部元素失败");
}
} catch (error) {
console.error(`Failed to create hand element for ${iface}:`, error);
logMessage("error", `创建 ${iface} 的手部配置失败: ${error.message}`);
}
});
// 延迟更新状态,确保DOM完全构建
setTimeout(() => {
updateEnabledHandsStatus();
logMessage(
"success",
`成功生成 ${Object.keys(handConfigs).length} 个手部配置`,
);
}, 100);
}
// 添加一个安全的 DOM 检查函数
function validateHandElement(handId) {
const element = document.getElementById(handId);
if (!element) {
console.error(`validateHandElement: 找不到元素 ${handId}`);
return false;
}
const requiredElements = [
`.hand-title`,
`#${handId}_checkbox`,
`#${handId}_interface`,
`#${handId}_handtype`,
`#${handId}_status_dot`,
`#${handId}_status_text`,
];
let isValid = true;
requiredElements.forEach((selector) => {
const el = selector.startsWith("#")
? document.getElementById(selector.slice(1))
: element.querySelector(selector);
if (!el) {
console.error(`validateHandElement: 在 ${handId} 中找不到 ${selector}`);
isValid = false;
}
});
return isValid;
}
// 增强的错误处理包装器
function safeUpdateHandElement(handId) {
try {
if (validateHandElement(handId)) {
updateHandElement(handId);
} else {
logMessage("error", `手部元素 ${handId} 验证失败,跳过更新`);
}
} catch (error) {
console.error(`Error updating hand element ${handId}:`, error);
logMessage("error", `更新手部元素 ${handId} 时出错:${error.message}`);
}
}
// 创建手部配置元素
function createHandElement(config) {
const div = document.createElement("div");
div.className = `hand-item ${config.enabled ? "enabled" : "disabled"}`;
div.id = config.id;
const handEmoji = config.handType === "left" ? "✋" : "🤚";
const handLabel = config.handType === "left" ? "左手" : "右手";
const handId = handTypeIds[config.handType];
// 确保 HTML 结构完整且正确
div.innerHTML = `
${handEmoji} ${
config.interface
} - ${handLabel}
CAN 接口
${availableInterfaces
.map(
(iface) =>
`${iface} `,
)
.join("")}
手型 (CAN ID: 0x${handId
.toString(16)
.toUpperCase()})
🤚 右手 (0x27)
✋ 左手 (0x28)
检查中...
`;
// 使用 requestAnimationFrame 确保 DOM 完全渲染后再设置事件监听器
requestAnimationFrame(() => {
setTimeout(() => {
setupHandEventListeners(config.id);
}, 0);
});
return div;
}
// 设置手部事件监听器
function setupHandEventListeners(handId) {
// 使用更安全的元素获取方式
const checkbox = document.getElementById(`${handId}_checkbox`);
const interfaceSelect = document.getElementById(`${handId}_interface`);
const handTypeSelect = document.getElementById(`${handId}_handtype`);
// 检查所有必需的元素是否存在
if (!checkbox) {
console.error(
`setupHandEventListeners: 找不到 checkbox - ${handId}_checkbox`,
);
return;
}
if (!interfaceSelect) {
console.error(
`setupHandEventListeners: 找不到 interfaceSelect - ${handId}_interface`,
);
return;
}
if (!handTypeSelect) {
console.error(
`setupHandEventListeners: 找不到 handTypeSelect - ${handId}_handtype`,
);
return;
}
// 移除现有的事件监听器(如果有的话)
checkbox.removeEventListener("change", checkbox._changeHandler);
interfaceSelect.removeEventListener("change", interfaceSelect._changeHandler);
handTypeSelect.removeEventListener("change", handTypeSelect._changeHandler);
// 创建新的事件处理器
checkbox._changeHandler = function () {
if (handConfigs[handId]) {
handConfigs[handId].enabled = this.checked;
updateHandElement(handId);
updateEnabledHandsStatus();
logMessage("info", `${handId}: ${this.checked ? "启用" : "禁用"}`);
}
};
interfaceSelect._changeHandler = function () {
if (handConfigs[handId]) {
handConfigs[handId].interface = this.value;
logMessage("info", `${handId}: 接口切换到 ${this.value}`);
checkSingleInterfaceStatus(handId);
}
};
handTypeSelect._changeHandler = function () {
if (handConfigs[handId]) {
handConfigs[handId].handType = this.value;
updateHandElement(handId);
const handName = this.value === "left" ? "左手" : "右手";
const handIdHex = handTypeIds[this.value];
logMessage(
"info",
`${handId}: 切换到${handName}模式 (0x${handIdHex
.toString(16)
.toUpperCase()})`,
);
}
};
// 添加事件监听器
checkbox.addEventListener("change", checkbox._changeHandler);
interfaceSelect.addEventListener("change", interfaceSelect._changeHandler);
handTypeSelect.addEventListener("change", handTypeSelect._changeHandler);
}
// 更新手部元素
function updateHandElement(handId) {
const config = handConfigs[handId];
const element = document.getElementById(handId);
// 添加安全检查
if (!element || !config) {
console.warn(`updateHandElement: 找不到元素或配置 - handId: ${handId}`);
return;
}
const handEmoji = config.handType === "left" ? "✋" : "🤚";
const handLabel = config.handType === "left" ? "左手" : "右手";
const handIdHex = handTypeIds[config.handType];
// 更新样式
element.className = `hand-item ${config.enabled ? "enabled" : "disabled"}`;
// 安全地更新标题
const title = element.querySelector(".hand-title");
if (title) {
title.textContent = `${handEmoji} ${config.interface} - ${handLabel}`;
} else {
console.warn(
`updateHandElement: 找不到 .hand-title 元素 - handId: ${handId}`,
);
}
// 安全地更新手型标签
const handTypeLabels = element.querySelectorAll(".control-label");
if (handTypeLabels.length >= 2) {
const handTypeLabel = handTypeLabels[1]; // 第二个 label 是手型的
if (handTypeLabel) {
handTypeLabel.textContent = `手型 (CAN ID: 0x${handIdHex
.toString(16)
.toUpperCase()})`;
}
} else {
console.warn(`updateHandElement: 找不到手型标签 - handId: ${handId}`);
}
// 确保选择框的值也同步更新
const handTypeSelect = document.getElementById(`${handId}_handtype`);
if (handTypeSelect) {
handTypeSelect.value = config.handType;
}
const interfaceSelect = document.getElementById(`${handId}_interface`);
if (interfaceSelect) {
interfaceSelect.value = config.interface;
}
const checkbox = document.getElementById(`${handId}_checkbox`);
if (checkbox) {
checkbox.checked = config.enabled;
}
}
// 更新启用手部状态显示
function updateEnabledHandsStatus() {
const enabledHands = Object.values(handConfigs).filter(
(config) => config.enabled,
);
const statusDiv = document.getElementById("enabled-hands-status");
if (enabledHands.length === 0) {
statusDiv.innerHTML =
'❌ 没有启用的手部 ';
} else {
const statusList = enabledHands
.map((config) => {
const emoji = config.handType === "left" ? "✋" : "🤚";
const handName = config.handType === "left" ? "左手" : "右手";
const statusDot = config.status === "online" ? "🟢" : "🔴";
return `${statusDot} ${emoji} ${config.interface} (${handName})`;
})
.join(" ");
statusDiv.innerHTML = statusList;
}
}
// 检查所有接口状态 - 修复错误处理
async function checkAllInterfaceStatus() {
try {
const response = await fetch("/api/status");
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (!data || data.status !== "success") {
throw new Error(data?.error || "获取状态失败");
}
// 安全地获取接口状态
const responseData = data.data || {};
interfaceStatus = responseData.interfaces || {};
updateAllHandStatus();
hideConnectionWarning();
} catch (error) {
logMessage("error", `状态检查失败:${error.message}`);
console.error("CheckAllInterfaceStatus Error:", error);
showConnectionWarning();
setAllHandStatusOffline();
}
}
// 检查单个接口状态
async function checkSingleInterfaceStatus(handId) {
await checkAllInterfaceStatus();
}
// 更新所有手部状态
function updateAllHandStatus() {
Object.keys(handConfigs).forEach((handId) => {
const config = handConfigs[handId];
const status = interfaceStatus[config.interface];
if (status && status.active) {
config.status = "online";
updateHandStatusDisplay(handId, "online", "在线");
} else {
config.status = "offline";
updateHandStatusDisplay(handId, "offline", "离线");
}
});
updateEnabledHandsStatus();
}
// 设置所有手部状态为离线
function setAllHandStatusOffline() {
Object.keys(handConfigs).forEach((handId) => {
handConfigs[handId].status = "offline";
updateHandStatusDisplay(handId, "offline", "连接失败");
});
updateEnabledHandsStatus();
}
// 更新手部状态显示
function updateHandStatusDisplay(handId, status, text) {
const statusDot = document.getElementById(`${handId}_status_dot`);
const statusText = document.getElementById(`${handId}_status_text`);
if (statusDot && statusText) {
statusDot.className = `status-dot ${status}`;
statusText.textContent = text;
}
}
// 显示连接警告
function showConnectionWarning() {
document.getElementById("connection-warning").style.display = "block";
}
// 隐藏连接警告
function hideConnectionWarning() {
document.getElementById("connection-warning").style.display = "none";
}
// 获取启用的手部配置
function getEnabledHands() {
return Object.values(handConfigs).filter((config) => config.enabled);
}
// 设置事件监听器
function setupEventListeners() {
const delayDefault = 30;
// 刷新所有接口按钮
document.getElementById("refresh-all").addEventListener("click", () => {
logMessage("info", "手动刷新所有接口...");
initializeSystem();
});
// 全局控制按钮
document
.getElementById("send-all-finger-poses")
.addEventListener("click", sendAllFingerPoses);
document
.getElementById("send-all-palm-poses")
.addEventListener("click", sendAllPalmPoses);
document
.getElementById("reset-all-hands")
.addEventListener("click", resetAllHands);
document
.getElementById("stop-all-animations")
.addEventListener("click", stopAllAnimations);
// 动画按钮
document
.getElementById("start-wave")
.addEventListener("click", () => startAnimationForAll("wave"));
document
.getElementById("start-sway")
.addEventListener("click", () => startAnimationForAll("sway"));
document
.getElementById("stop-animation")
.addEventListener("click", stopAllAnimations);
// 预设姿势按钮 - 使用 LinkerHandController 的预设
setupPresetButtons();
// 数字手势按钮事件
setupNumericPresets();
// Refill core 按钮
setupRefillCore();
}
// 设置预设按钮
function setupPresetButtons() {
const delayDefault = 30;
// 基础预设姿势
const presets = {
"pose-fist": { finger: "FIST", palm: null },
"pose-open": { finger: "OPEN", palm: null },
"pose-pinch": { finger: "PINCH", palm: "PALM_PINCH" },
"pose-point": { finger: "POINT", palm: null },
"pose-thumbs-up": { finger: "THUMBSUP", palm: null },
"pose-yeah": { finger: "YEAH", palm: "PALM_YEAH" },
"pose-wave": { finger: "WAVE", palm: null },
"pose-big-fist": { finger: "BIG_FIST", palm: "PALM_BIG_FIST" },
"pose-big-open": { finger: "BIG_OPEN", palm: "PALM_BIG_OPEN" },
"pose-yo": { finger: "YO", palm: null },
"pose-gun": { finger: "GUN", palm: "PALM_GUN" },
"pose-ok": { finger: "OK", palm: "PALM_OK" },
};
Object.entries(presets).forEach(([id, preset]) => {
const button = document.getElementById(id);
if (button) {
button.addEventListener("click", () => {
if (preset.palm) {
LinkerHandController.applyPalmPreset(
LinkerHandController.PRESETS[preset.palm],
);
const palmPose = LinkerHandController.getPalmPoseValues();
LinkerHandController.sendPalmPoseToAll(palmPose);
setTimeout(() => {
LinkerHandController.applyFingerPreset(
LinkerHandController.PRESETS[preset.finger],
);
const fingerPose = LinkerHandController.getFingerPoseValues();
LinkerHandController.sendFingerPoseToAll(fingerPose);
}, delayDefault);
} else {
LinkerHandController.applyFingerPreset(
LinkerHandController.PRESETS[preset.finger],
);
const fingerPose = LinkerHandController.getFingerPoseValues();
LinkerHandController.sendFingerPoseToAll(fingerPose);
}
});
}
});
}
// 设置数字预设
function setupNumericPresets() {
const delayDefault = 30;
// 数字 1-9 的预设
for (let i = 1; i <= 9; i++) {
const button = document.getElementById(`pose-${i}`);
if (button) {
button.addEventListener("click", () => {
const palmPreset =
LinkerHandController.PRESETS[`PALM_${getNumberName(i)}`];
const fingerPreset = LinkerHandController.PRESETS[getNumberName(i)];
if (palmPreset) {
LinkerHandController.applyPalmPreset(palmPreset);
const palmPose = LinkerHandController.getPalmPoseValues();
LinkerHandController.sendPalmPoseToAll(palmPose);
}
setTimeout(() => {
if (fingerPreset) {
LinkerHandController.applyFingerPreset(fingerPreset);
const fingerPose = LinkerHandController.getFingerPoseValues();
LinkerHandController.sendFingerPoseToAll(fingerPose);
}
}, delayDefault);
});
}
}
}
// 获取数字名称
function getNumberName(num) {
const names = [
"",
"ONE",
"TWO",
"THREE",
"FOUR",
"FIVE",
"SIX",
"SEVEN",
"EIGHT",
"NINE",
];
return names[num] || "";
}
// 设置 Refill Core 功能
function setupRefillCore() {
document.getElementById("refill-core").addEventListener("click", () => {
event.preventDefault();
event.stopPropagation();
console.log("refill-core");
const rukaPoseList = [
[
[246, 188, 128, 128],
[149, 30, 145, 36, 41, 46],
], // 食指
[
[246, 155, 154, 66],
[138, 80, 0, 154, 41, 46],
], // 中指
[
[246, 155, 154, 40],
[140, 80, 0, 15, 155, 46],
], // 无名指
[
[246, 155, 154, 25],
[140, 62, 0, 15, 29, 143],
], // 小指
];
const delayTime = 350; // 设定延迟时间为 350ms
// 创建完整的序列:从第一个到最后一个,再从最后一个回到第二个
const forwardIndices = [...Array(rukaPoseList.length).keys()]; // [0,1,2,3]
const backwardIndices = [...forwardIndices].reverse().slice(1); // [3,2,1]
const sequenceIndices = [...forwardIndices, ...backwardIndices];
// 遍历序列索引,为每个索引创建两个操作(palm 和 finger)
sequenceIndices.forEach((index, step) => {
const targetPose = rukaPoseList[index];
// 应用 palm 预设
setTimeout(() => {
console.log(
`Step ${step + 1}a: Applying palm preset for pose ${index + 1}`,
);
LinkerHandController.applyPalmPreset(targetPose[0]);
const palmPose = LinkerHandController.getPalmPoseValues();
LinkerHandController.sendPalmPoseToAll(palmPose);
}, delayTime * (step * 2)); // 每个完整步骤有两个操作,所以是 step*2
// 应用 finger 预设
setTimeout(() => {
console.log(
`Step ${step + 1}b: Applying finger preset for pose ${index + 1}`,
);
LinkerHandController.applyFingerPreset(targetPose[1]);
const fingerPose = LinkerHandController.getFingerPoseValues();
LinkerHandController.sendFingerPoseToAll(fingerPose);
}, delayTime * (step * 2 + 1)); // 偏移一个 delayTime
});
});
}
// 设置滑块事件
function setupSliderEvents() {
// 手指滑块
for (let i = 0; i < 6; i++) {
const slider = document.getElementById(`finger${i}`);
const valueDisplay = document.getElementById(`finger${i}-value`);
slider.addEventListener("input", function () {
valueDisplay.textContent = this.value;
});
}
// 掌部滑块
for (let i = 0; i < 4; i++) {
const slider = document.getElementById(`palm${i}`);
const valueDisplay = document.getElementById(`palm${i}-value`);
slider.addEventListener("input", function () {
valueDisplay.textContent = this.value;
});
}
// 速度滑块
const speedSlider = document.getElementById("animation-speed");
const speedDisplay = document.getElementById("speed-value");
speedSlider.addEventListener("input", function () {
speedDisplay.textContent = this.value;
});
}
// 发送所有启用手部的手指姿态
async function sendAllFingerPoses() {
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
const pose = [];
for (let i = 0; i < 6; i++) {
pose.push(Number.parseInt(document.getElementById(`finger${i}`).value));
}
logMessage("info", `发送手指姿态到 ${enabledHands.length} 个启用的手部...`);
for (const config of enabledHands) {
await sendFingerPoseToHand(config, pose);
}
}
// 发送所有启用手部的掌部姿态
async function sendAllPalmPoses() {
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
const pose = [];
for (let i = 0; i < 4; i++) {
pose.push(Number.parseInt(document.getElementById(`palm${i}`).value));
}
logMessage("info", `发送掌部姿态到 ${enabledHands.length} 个启用的手部...`);
for (const config of enabledHands) {
await sendPalmPoseToHand(config, pose);
}
}
// 发送手指姿态到指定手部
async function sendFingerPoseToHand(config, pose) {
try {
const response = await fetch("/api/fingers", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
interface: config.interface,
pose: pose,
handType: config.handType,
handId: handTypeIds[config.handType],
}),
});
const data = await response.json();
if (data.status === "success") {
const handName = config.handType === "left" ? "左手" : "右手";
logMessage(
"success",
`${config.interface} (${handName}): 手指姿态发送成功 [${pose.join(
", ",
)}]`,
);
} else {
logMessage("error", `${config.interface}: ${data.error}`);
}
} catch (error) {
logMessage("error", `${config.interface}: 发送失败 - ${error.message}`);
}
}
// 发送掌部姿态到指定手部
async function sendPalmPoseToHand(config, pose) {
try {
const response = await fetch("/api/palm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
interface: config.interface,
pose: pose,
handType: config.handType,
handId: handTypeIds[config.handType],
}),
});
const data = await response.json();
if (data.status === "success") {
const handName = config.handType === "left" ? "左手" : "右手";
logMessage(
"success",
`${config.interface} (${handName}): 掌部姿态发送成功 [${pose.join(
", ",
)}]`,
);
} else {
logMessage("error", `${config.interface}: ${data.error}`);
}
} catch (error) {
logMessage("error", `${config.interface}: 发送失败 - ${error.message}`);
}
}
// 为所有启用手部设置预设姿势
async function setPresetPoseForAll(preset) {
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
logMessage(
"info",
`设置预设姿势 "${preset}" 到 ${enabledHands.length} 个启用的手部...`,
);
for (const config of enabledHands) {
await setPresetPoseToHand(config, preset);
}
}
// 为指定手部设置预设姿势
async function setPresetPoseToHand(config, preset) {
try {
const response = await fetch(
`/api/preset/${preset}?interface=${config.interface}&handType=${config.handType}`,
{
method: "POST",
},
);
const data = await response.json();
if (data.status === "success") {
const handName = config.handType === "left" ? "左手" : "右手";
logMessage(
"success",
`${config.interface} (${handName}): ${data.message}`,
);
} else {
logMessage("error", `${config.interface}: ${data.error}`);
}
} catch (error) {
logMessage("error", `${config.interface}: 预设姿势失败 - ${error.message}`);
}
}
// 为所有启用手部启动动画
async function startAnimationForAll(type) {
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
const speed = Number.parseInt(
document.getElementById("animation-speed").value,
);
logMessage(
"info",
`启动 "${type}" 动画到 ${enabledHands.length} 个启用的手部...`,
);
for (const config of enabledHands) {
await startAnimationForHand(config, type, speed);
}
}
// 为指定手部启动动画
async function startAnimationForHand(config, type, speed) {
try {
const response = await fetch("/api/animation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
interface: config.interface,
type: type,
speed: speed,
handType: config.handType,
handId: handTypeIds[config.handType],
}),
});
const data = await response.json();
if (data.status === "success") {
const handName = config.handType === "left" ? "左手" : "右手";
logMessage(
"success",
`${config.interface} (${handName}): ${data.message}`,
);
} else {
logMessage("error", `${config.interface}: ${data.error}`);
}
} catch (error) {
logMessage("error", `${config.interface}: 动画启动失败 - ${error.message}`);
}
}
// 停止所有启用手部的动画
async function stopAllAnimations() {
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
logMessage("info", `停止 ${enabledHands.length} 个启用手部的动画...`);
for (const config of enabledHands) {
await stopAnimationForHand(config);
}
}
// 停止指定手部的动画
async function stopAnimationForHand(config) {
try {
const response = await fetch("/api/animation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
interface: config.interface,
type: "stop",
handType: config.handType,
handId: handTypeIds[config.handType],
}),
});
const data = await response.json();
if (data.status === "success") {
const handName = config.handType === "left" ? "左手" : "右手";
logMessage(
"success",
`${config.interface} (${handName}): ${data.message}`,
);
} else {
logMessage("error", `${config.interface}: ${data.error}`);
}
} catch (error) {
logMessage("error", `${config.interface}: 停止动画失败 - ${error.message}`);
}
}
// 重置所有启用手部
async function resetAllHands() {
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
// 重置滑块值
LinkerHandController.applyFingerPreset(LinkerHandController.PRESETS.OPEN);
LinkerHandController.applyPalmPreset(
LinkerHandController.PRESETS.PALM_NEUTRAL,
);
logMessage("info", `重置 ${enabledHands.length} 个启用的手部...`);
// 停止所有动画
await stopAllAnimations();
// 发送重置姿态
await sendAllFingerPoses();
await sendAllPalmPoses();
logMessage("info", "所有启用手部已重置完成");
}
// 自动触发按钮序列(数字手势)
async function triggerButtonsSequentially(interval = 2000) {
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
logMessage("info", `开始自动数字手势序列 (${enabledHands.length} 个手部)`);
const buttons = [
document.getElementById("pose-1"),
document.getElementById("pose-2"),
document.getElementById("pose-3"),
document.getElementById("pose-4"),
document.getElementById("pose-5"),
document.getElementById("pose-6"),
document.getElementById("pose-7"),
document.getElementById("pose-8"),
document.getElementById("pose-9"),
];
for (const button of buttons) {
if (button) {
button.click();
await new Promise((resolve) => setTimeout(resolve, interval));
}
}
// 然后执行所有预设手势
const presetButtons = document.querySelectorAll(
".preset-grid button:not(.preset-num-pose)",
);
for (const button of presetButtons) {
button.click();
await new Promise((resolve) => setTimeout(resolve, interval));
}
logMessage("success", "数字手势序列完成");
}
// 日志消息
function logMessage(type, message) {
const statusLog = document.getElementById("status-log");
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement("div");
logEntry.className = "log-entry";
let statusClass = "status-info";
if (type === "success") statusClass = "status-success";
else if (type === "error") statusClass = "status-error";
logEntry.innerHTML = `
${timestamp}
${message}
`;
statusLog.appendChild(logEntry);
statusLog.scrollTop = statusLog.scrollHeight;
// 保持最多 50 条日志
const entries = statusLog.querySelectorAll(".log-entry");
if (entries.length > 50) {
statusLog.removeChild(entries[0]);
}
}
// 启动状态更新器
function startStatusUpdater() {
// 每 5 秒检查一次接口状态
setInterval(async () => {
await checkAllInterfaceStatus();
}, 5000);
// 每 30 秒刷新一次接口列表
setInterval(async () => {
const oldInterfaces = [...availableInterfaces];
await loadAvailableInterfaces();
// 如果接口发生变化,重新生成配置
if (JSON.stringify(oldInterfaces) !== JSON.stringify(availableInterfaces)) {
generateHandConfigs();
}
}, 30000);
}
// 添加调试功能
async function debugSystemStatus() {
logMessage("info", "🔍 开始系统调试...");
// 检查 HTML 元素
const elements = {
"hands-grid": document.getElementById("hands-grid"),
"status-log": document.getElementById("status-log"),
"enabled-hands-status": document.getElementById("enabled-hands-status"),
"sensor-data": document.getElementById("sensor-data"),
};
Object.entries(elements).forEach(([name, element]) => {
if (element) {
logMessage("success", `✅ 元素 ${name} 存在`);
} else {
logMessage("error", `❌ 元素 ${name} 不存在`);
}
});
// 检查全局变量
logMessage("info", `可用接口:[${availableInterfaces.join(", ")}]`);
logMessage("info", `手部配置数量:${Object.keys(handConfigs).length}`);
logMessage("info", `启用手部数量:${getEnabledHands().length}`);
// 测试 API 连通性
try {
logMessage("info", "测试 /api/health 连接...");
const response = await fetch("/api/health");
if (response.ok) {
const data = await response.json();
logMessage("success", "✅ 健康检查通过");
console.log("Health Check Data:", data);
} else {
logMessage("error", `❌ 健康检查失败:HTTP ${response.status}`);
}
} catch (error) {
logMessage("error", `❌ 健康检查异常:${error.message}`);
}
// 测试接口 API
try {
logMessage("info", "测试 /api/interfaces 连接...");
const response = await fetch("/api/interfaces");
if (response.ok) {
const data = await response.json();
logMessage("success", "✅ 接口 API 通过");
console.log("Interfaces API Data:", data);
} else {
logMessage("error", `❌ 接口 API 失败:HTTP ${response.status}`);
}
} catch (error) {
logMessage("error", `❌ 接口 API 异常:${error.message}`);
}
}
// 导出全局函数供 HTML 按钮使用
window.triggerButtonsSequentially = triggerButtonsSequentially;
window.debugSystemStatus = debugSystemStatus;
// 添加全局错误处理
window.addEventListener("error", (event) => {
logMessage("error", `全局错误:${event.error?.message || event.message}`);
console.error("Global Error:", event.error);
});
window.addEventListener("unhandledrejection", (event) => {
logMessage(
"error",
`未处理的 Promise 拒绝:${event.reason?.message || event.reason}`,
);
console.error("Unhandled Promise Rejection:", event.reason);
});
// 页面可见性变化时的处理
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
// 页面变为可见时,刷新状态
checkAllInterfaceStatus();
}
});
// 处理网络错误时的重连逻辑
window.addEventListener("online", () => {
logMessage("info", "网络连接已恢复,正在重新连接...");
initializeSystem();
});
window.addEventListener("offline", () => {
logMessage("error", "网络连接已断开");
showConnectionWarning();
});
// 键盘快捷键支持
document.addEventListener("keydown", (e) => {
// Ctrl+R 刷新接口
if (e.ctrlKey && e.key === "r") {
e.preventDefault();
logMessage("info", "快捷键刷新接口列表...");
initializeSystem();
}
// Ctrl+Space 停止所有动画
if (e.ctrlKey && e.code === "Space") {
e.preventDefault();
stopAllAnimations();
}
// Ctrl+A 选择/取消选择所有手部
if (e.ctrlKey && e.key === "a") {
e.preventDefault();
toggleAllHands();
}
// 数字键 1-9 快速设置预设姿势
if (e.key >= "1" && e.key <= "9" && !e.ctrlKey && !e.altKey) {
const activeElement = document.activeElement;
// 确保不在输入框中
if (
activeElement.tagName !== "INPUT" &&
activeElement.tagName !== "SELECT"
) {
const button = document.getElementById(`pose-${e.key}`);
if (button) button.click();
}
}
});
// 切换所有手部启用状态
function toggleAllHands() {
const enabledCount = Object.values(handConfigs).filter(
(config) => config.enabled,
).length;
const shouldEnable = enabledCount === 0;
Object.keys(handConfigs).forEach((handId) => {
handConfigs[handId].enabled = shouldEnable;
const checkbox = document.getElementById(`${handId}_checkbox`);
if (checkbox) {
checkbox.checked = shouldEnable;
}
updateHandElement(handId);
});
updateEnabledHandsStatus();
logMessage("info", `${shouldEnable ? "启用" : "禁用"}所有手部`);
}
// 工具提示功能
function addTooltips() {
const tooltips = {
"refresh-all": "刷新所有可用接口列表",
"send-all-finger-poses": "向所有启用的手部发送当前手指关节位置",
"send-all-palm-poses": "向所有启用的手部发送当前掌部关节位置",
"reset-all-hands": "重置所有启用手部到默认位置",
"stop-all-animations": "停止所有启用手部的动画",
"start-wave": "启动所有启用手部的手指波浪动画",
"start-sway": "启动所有启用手部的掌部摆动动画",
"stop-animation": "停止所有启用手部的动画",
"refill-core": "执行 Refill Core 动作序列",
};
Object.entries(tooltips).forEach(([id, text]) => {
const element = document.getElementById(id);
if (element) {
element.title = text;
}
});
}
// 页面加载完成后添加工具提示
document.addEventListener("DOMContentLoaded", () => {
addTooltips();
});
// ---eof
// 六手依次动画函数
async function startSequentialHandAnimation(
animationType = "wave",
interval = 500,
cycles = 3,
) {
const enabledHands = getEnabledHands();
// 检查是否有足够的手部
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
// 确保按接口名称排序(can0, can1, can2...)
const sortedHands = enabledHands.sort((a, b) => {
const getInterfaceNumber = (iface) => {
const match = iface.match(/(\d+)$/);
return match ? Number.parseInt(match[1]) : 0;
};
return getInterfaceNumber(a.interface) - getInterfaceNumber(b.interface);
});
logMessage(
"info",
`开始六手依次动画 - 类型:${animationType}, 间隔:${interval}ms, 循环:${cycles}次`,
);
logMessage(
"info",
`动画顺序:${sortedHands.map((h) => h.interface).join(" → ")}`,
);
// 定义动画预设
const animationPresets = {
wave: {
name: "手指波浪",
fingerPoses: [
[255, 255, 255, 255, 255, 255], // 完全张开
[128, 128, 128, 128, 128, 128], // 中间位置
[64, 64, 64, 64, 64, 64], // 握拳
[128, 128, 128, 128, 128, 128], // 回到中间
],
palmPose: [128, 128, 128, 128], // 掌部保持中立
},
thumbsUp: {
name: "竖拇指传递",
fingerPoses: [
[255, 255, 0, 0, 0, 0], // 竖拇指
[128, 128, 128, 128, 128, 128], // 恢复中立
],
palmPose: [128, 128, 128, 128],
},
point: {
name: "食指指点传递",
fingerPoses: [
[0, 0, 255, 0, 0, 0], // 食指指点
[128, 128, 128, 128, 128, 128], // 恢复中立
],
palmPose: [128, 128, 128, 128],
},
fistOpen: {
name: "握拳张开",
fingerPoses: [
[64, 64, 64, 64, 64, 64], // 握拳
[255, 255, 255, 255, 255, 255], // 张开
[128, 128, 128, 128, 128, 128], // 中立
],
palmPose: [128, 128, 128, 128],
},
numbers: {
name: "数字倒计时",
fingerPoses: [
[255, 255, 255, 255, 255, 255], // 5
[0, 57, 255, 255, 255, 255], // 4
[0, 57, 255, 255, 255, 0], // 3
[0, 57, 255, 255, 0, 0], // 2
[0, 57, 255, 0, 0, 0], // 1
[64, 64, 64, 64, 64, 64], // 握拳 (0)
],
palmPoses: [
[255, 109, 255, 118], // 5 对应的掌部
[255, 109, 255, 118], // 4 对应的掌部
[255, 109, 255, 118], // 3 对应的掌部
[255, 109, 255, 118], // 2 对应的掌部
[255, 109, 255, 118], // 1 对应的掌部
[128, 128, 128, 128], // 0 对应的掌部
],
},
mexican: {
name: "墨西哥波浪",
fingerPoses: [
[64, 64, 64, 64, 64, 64], // 起始握拳
[128, 64, 64, 64, 64, 64], // 拇指起
[255, 128, 64, 64, 64, 64], // 拇指 + 食指起
[255, 255, 128, 64, 64, 64], // 前三指起
[255, 255, 255, 128, 64, 64], // 前四指起
[255, 255, 255, 255, 128, 64], // 前五指起
[255, 255, 255, 255, 255, 255], // 全部张开
[128, 255, 255, 255, 255, 128], // 波浪形
[64, 128, 255, 255, 128, 64], // 继续波浪
[64, 64, 128, 255, 128, 64], // 波浪收尾
[64, 64, 64, 128, 64, 64], // 几乎回到握拳
[64, 64, 64, 64, 64, 64], // 完全握拳
],
palmPose: [128, 128, 128, 128],
},
};
const preset = animationPresets[animationType] || animationPresets.wave;
const fingerPoses = preset.fingerPoses;
const palmPoses =
preset.palmPoses || Array(fingerPoses.length).fill(preset.palmPose);
// 执行动画循环
for (let cycle = 0; cycle < cycles; cycle++) {
logMessage("info", `${preset.name} - 第 ${cycle + 1}/${cycles} 轮`);
// 每个动作姿势
for (let poseIndex = 0; poseIndex < fingerPoses.length; poseIndex++) {
const currentFingerPose = fingerPoses[poseIndex];
const currentPalmPose = palmPoses[poseIndex];
// 依次激活每只手
for (let handIndex = 0; handIndex < sortedHands.length; handIndex++) {
const hand = sortedHands[handIndex];
const handName = hand.handType === "left" ? "左手" : "右手";
// 先发送掌部姿态
await sendPalmPoseToHand(hand, currentPalmPose);
// 短暂延迟后发送手指姿态
setTimeout(async () => {
await sendFingerPoseToHand(hand, currentFingerPose);
}, 50);
logMessage(
"info",
`${hand.interface}(${handName}) 执行姿势 ${poseIndex + 1}/${
fingerPoses.length
}`,
);
// 等待间隔时间再激活下一只手
await new Promise((resolve) => setTimeout(resolve, interval));
}
}
// 循环间隔(如果有多轮)
if (cycle < cycles - 1) {
logMessage("info", `等待下一轮动画...`);
await new Promise((resolve) => setTimeout(resolve, interval * 2));
}
}
// 动画结束后,让所有手回到中立位置
logMessage("info", "动画完成,恢复中立位置...");
const neutralFingerPose = [128, 128, 128, 128, 128, 128];
const neutralPalmPose = [128, 128, 128, 128];
for (const hand of sortedHands) {
await sendPalmPoseToHand(hand, neutralPalmPose);
setTimeout(async () => {
await sendFingerPoseToHand(hand, neutralFingerPose);
}, 50);
await new Promise((resolve) => setTimeout(resolve, 100));
}
logMessage("success", `六手依次动画 "${preset.name}" 完成!`);
}
// 扩展的动画控制函数
async function startCustomSequentialAnimation(config) {
const {
animationType = "wave",
interval = 500,
cycles = 3,
direction = "forward", // 'forward', 'backward', 'bounce'
simultaneousHands = 1, // 同时激活的手数
staggerDelay = 100, // 同时激活手之间的错开延迟
} = config;
const enabledHands = getEnabledHands();
if (enabledHands.length === 0) {
logMessage("error", "没有启用的手部");
return;
}
// 根据方向排序手部
let sortedHands = enabledHands.sort((a, b) => {
const getInterfaceNumber = (iface) => {
const match = iface.match(/(\d+)$/);
return match ? Number.parseInt(match[1]) : 0;
};
return getInterfaceNumber(a.interface) - getInterfaceNumber(b.interface);
});
if (direction === "backward") {
sortedHands = sortedHands.reverse();
}
logMessage(
"info",
`开始自定义六手动画 - 方向:${direction}, 同时手数:${simultaneousHands}`,
);
// 执行动画逻辑...
// 这里可以根据 simultaneousHands 参数同时控制多只手
// 实现类似的动画逻辑,但支持更多自定义选项
}
// 预定义的快捷动画函数
async function startWaveAnimation() {
await startSequentialHandAnimation("wave", 300, 2);
}
async function startThumbsUpRelay() {
await startSequentialHandAnimation("thumbsUp", 400, 3);
}
async function startPointingRelay() {
await startSequentialHandAnimation("point", 350, 2);
}
async function startNumberCountdown() {
await startSequentialHandAnimation("numbers", 800, 1);
}
async function startMexicanWave() {
await startSequentialHandAnimation("mexican", 200, 3);
}
async function startFistOpenWave() {
await startSequentialHandAnimation("fistOpen", 400, 2);
}
// 高级组合动画:先正向再反向
async function startBidirectionalWave() {
logMessage("info", "开始双向波浪动画...");
// 正向波浪
await startSequentialHandAnimation("wave", 300, 1);
await new Promise((resolve) => setTimeout(resolve, 500));
// 反向波浪(通过反转手部顺序实现)
const originalGetEnabledHands = window.getEnabledHands;
window.getEnabledHands = () => originalGetEnabledHands().reverse();
await startSequentialHandAnimation("wave", 300, 1);
// 恢复原始函数
window.getEnabledHands = originalGetEnabledHands;
logMessage("success", "双向波浪动画完成!");
}
// 导出函数到全局作用域
window.startSequentialHandAnimation = startSequentialHandAnimation;
window.startCustomSequentialAnimation = startCustomSequentialAnimation;
window.startWaveAnimation = startWaveAnimation;
window.startThumbsUpRelay = startThumbsUpRelay;
window.startPointingRelay = startPointingRelay;
window.startNumberCountdown = startNumberCountdown;
window.startMexicanWave = startMexicanWave;
window.startFistOpenWave = startFistOpenWave;
window.startBidirectionalWave = startBidirectionalWave;