// 全局变量
let availableInterfaces = [];
let interfaceStatus = {};
let handConfigs = {}; // 存储每个手的配置
let 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: function (func, delay) {
let timer;
return function () {
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: function () {
const pose = [];
for (let i = 0; i < 6; i++) {
pose.push(parseInt(document.getElementById(`finger${i}`).value));
}
return pose;
},
// 获取掌部姿态值
getPalmPoseValues: function () {
const pose = [];
for (let i = 0; i < 4; i++) {
pose.push(parseInt(document.getElementById(`palm${i}`).value));
}
return pose;
},
// 设置手指滑块值
applyFingerPreset: function (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: function (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: function (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: function (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: function (label, value) {
if (value === undefined || value === null) value = 0;
return `
${label}
${value}%
`;
}
};
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
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', function() {
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(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(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 = 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', function(event) {
logMessage('error', `全局错误: ${event.error?.message || event.message}`);
console.error('Global Error:', event.error);
});
window.addEventListener('unhandledrejection', function(event) {
logMessage('error', `未处理的Promise拒绝: ${event.reason?.message || event.reason}`);
console.error('Unhandled Promise Rejection:', event.reason);
});
// 页面可见性变化时的处理
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
// 页面变为可见时,刷新状态
checkAllInterfaceStatus();
}
});
// 处理网络错误时的重连逻辑
window.addEventListener('online', function() {
logMessage('info', '网络连接已恢复,正在重新连接...');
initializeSystem();
});
window.addEventListener('offline', function() {
logMessage('error', '网络连接已断开');
showConnectionWarning();
});
// 键盘快捷键支持
document.addEventListener('keydown', function(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', function() {
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 ? 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 ? 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 = function() {
return 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;