优化控制界面UI样式,增加外界手柄控制
This commit is contained in:
parent
7fbe4f59cd
commit
bd13a5aeb2
@ -6,14 +6,16 @@
|
||||
justify-content: center;
|
||||
min-height: 80vh;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 手柄风格布局 */
|
||||
@ -23,46 +25,66 @@
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
/* 移动控制按钮区域 */
|
||||
.movement-controls {
|
||||
/* 控制器布局 */
|
||||
.controller-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
padding: 30px;
|
||||
background: linear-gradient(145deg, #e6e6e6, #ffffff);
|
||||
border-radius: 20px;
|
||||
box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.1), -10px -10px 20px rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 方向键区域 */
|
||||
.dpad {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.movement-row {
|
||||
.dpad-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 录制回放按钮区域 */
|
||||
.record-controls {
|
||||
/* 中间录制回放控制 */
|
||||
.center-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 右侧旋转控制 */
|
||||
.rotate-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
width: 120px;
|
||||
height: 50px;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
border-radius: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1), -5px -5px 10px rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
@ -81,93 +103,168 @@
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 方向按钮样式 */
|
||||
.btn-forward {
|
||||
background-color: #4CAF50;
|
||||
background: linear-gradient(145deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-backward {
|
||||
background-color: #f44336;
|
||||
background: linear-gradient(145deg, #f44336, #da190b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-left {
|
||||
background-color: #2196F3;
|
||||
background: linear-gradient(145deg, #2196F3, #1976D2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-right {
|
||||
background-color: #FF9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-rotate-left {
|
||||
background-color: #9C27B0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-rotate-right {
|
||||
background-color: #607D8B;
|
||||
background: linear-gradient(145deg, #FF9800, #F57C00);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 斜向按钮样式 */
|
||||
.btn-forward-left {
|
||||
background-color: #4CAF50;
|
||||
background: linear-gradient(145deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-forward-right {
|
||||
background-color: #4CAF50;
|
||||
background: linear-gradient(145deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-backward-left {
|
||||
background-color: #f44336;
|
||||
background: linear-gradient(145deg, #f44336, #da190b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-backward-right {
|
||||
background-color: #f44336;
|
||||
background: linear-gradient(145deg, #f44336, #da190b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 旋转按钮样式 */
|
||||
.btn-rotate-left {
|
||||
background: linear-gradient(145deg, #9C27B0, #7B1FA2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-rotate-right {
|
||||
background: linear-gradient(145deg, #607D8B, #455A64);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 中心方块样式 */
|
||||
.btn-center {
|
||||
background: linear-gradient(145deg, #666666, #444444);
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1), -5px -5px 10px rgba(255, 255, 255, 0.7);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn-center:hover {
|
||||
transform: none;
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1), -5px -5px 10px rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.btn-center:active {
|
||||
transform: none;
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1), -5px -5px 10px rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 录制回放按钮样式 */
|
||||
.btn-record {
|
||||
background: linear-gradient(145deg, #FF5722, #E64A19);
|
||||
color: white;
|
||||
width: 120px;
|
||||
height: 50px;
|
||||
font-size: 16px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.btn-playback {
|
||||
background: linear-gradient(145deg, #00BCD4, #0097A7);
|
||||
color: white;
|
||||
width: 120px;
|
||||
height: 50px;
|
||||
font-size: 16px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
/* 按钮动画效果 */
|
||||
.btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 8px 8px 16px rgba(0, 0, 0, 0.15), -8px -8px 16px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.2), -3px -3px 6px rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* 状态显示 */
|
||||
.status {
|
||||
margin-top: 30px;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10px;
|
||||
min-height: 50px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(145deg, #f5f5f5, #e0e0e0);
|
||||
border-radius: 15px;
|
||||
min-height: 60px;
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 5px 5px 10px rgba(0, 0, 0, 0.1), inset -5px -5px 10px rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.status p {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 400px) {
|
||||
@media (max-width: 650px) {
|
||||
.controller-layout {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dpad-row {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-record, .btn-playback {
|
||||
width: 100px;
|
||||
height: 45px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-record, .btn-playback {
|
||||
width: 90px;
|
||||
height: 40px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@ -56,6 +56,16 @@ h1 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-record {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-playback {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
@ -424,6 +424,8 @@ function stopMotor() {
|
||||
const timestamp = Date.now() - window.recordingStartTime;
|
||||
window.recordedActions.push({ direction: 'stop', timestamp });
|
||||
console.log('Recorded action:', { direction: 'stop', timestamp });
|
||||
} else {
|
||||
console.log('Not recording, action:', 'stop');
|
||||
}
|
||||
|
||||
// 发送停止请求到服务器
|
||||
@ -437,7 +439,8 @@ function stopMotor() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// 停止成功,不显示消息
|
||||
// 停止成功,显示已停止消息
|
||||
status.innerHTML = `<p>${data.message}</p>`;
|
||||
} else {
|
||||
status.innerHTML = `<p>错误: ${data.message}</p>`;
|
||||
}
|
||||
|
||||
247
main/Javascript/joycon.js
Normal file
247
main/Javascript/joycon.js
Normal file
@ -0,0 +1,247 @@
|
||||
// 确保在DOM加载完成后执行
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('DOMContentLoaded - Joycon.js executing');
|
||||
|
||||
// 游戏手柄相关变量
|
||||
let gamepad = null;
|
||||
let lastDirection = 'stop';
|
||||
let isRecording = false;
|
||||
let isPlaying = false;
|
||||
let recordedActions = [];
|
||||
let recordingStartTime = 0;
|
||||
|
||||
// 按键状态标志(用于检测按键按下事件)
|
||||
let recordButtonPressed = false;
|
||||
let playbackButtonPressed = false;
|
||||
|
||||
// 获取状态元素
|
||||
const status = document.getElementById('status');
|
||||
|
||||
// 检查浏览器是否支持游戏手柄API
|
||||
if (!('getGamepads' in navigator)) {
|
||||
status.innerHTML = '<p>您的浏览器不支持游戏手柄API</p>';
|
||||
console.log('Gamepad API not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
// 连接游戏手柄
|
||||
window.addEventListener('gamepadconnected', function(e) {
|
||||
gamepad = e.gamepad;
|
||||
status.innerHTML = `<p>已连接游戏手柄: ${gamepad.id}</p>`;
|
||||
console.log('Gamepad connected:', gamepad);
|
||||
// 开始监听游戏手柄输入
|
||||
requestAnimationFrame(updateGamepad);
|
||||
});
|
||||
|
||||
// 断开游戏手柄
|
||||
window.addEventListener('gamepaddisconnected', function(e) {
|
||||
status.innerHTML = '<p>游戏手柄已断开连接</p>';
|
||||
console.log('Gamepad disconnected:', e.gamepad);
|
||||
gamepad = null;
|
||||
});
|
||||
|
||||
// 游戏手柄输入更新函数
|
||||
function updateGamepad() {
|
||||
if (!gamepad) {
|
||||
requestAnimationFrame(updateGamepad);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取游戏手柄状态
|
||||
const gamepads = navigator.getGamepads();
|
||||
gamepad = gamepads[gamepad.index];
|
||||
|
||||
if (!gamepad) {
|
||||
requestAnimationFrame(updateGamepad);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理摇杆输入 (通常是0号摇杆,左摇杆)
|
||||
const leftStickX = gamepad.axes[0];
|
||||
const leftStickY = gamepad.axes[1];
|
||||
|
||||
// 处理按键输入
|
||||
const buttons = gamepad.buttons;
|
||||
|
||||
// 摇杆死区
|
||||
const deadzone = 0.3;
|
||||
|
||||
// 确定移动方向
|
||||
let direction = 'stop';
|
||||
|
||||
if (Math.abs(leftStickX) > deadzone || Math.abs(leftStickY) > deadzone) {
|
||||
// 计算角度
|
||||
const angle = Math.atan2(leftStickY, leftStickX) * 180 / Math.PI;
|
||||
|
||||
// 根据角度确定方向(修复:交换前后方向,修复:使用下划线分隔的方向值)
|
||||
if (angle >= -22.5 && angle < 22.5) {
|
||||
direction = 'right'; // 右
|
||||
} else if (angle >= 22.5 && angle < 67.5) {
|
||||
direction = 'backward_right'; // 右后
|
||||
} else if (angle >= 67.5 && angle < 112.5) {
|
||||
direction = 'backward'; // 后
|
||||
} else if (angle >= 112.5 && angle < 157.5) {
|
||||
direction = 'backward_left'; // 左后
|
||||
} else if (angle >= 157.5 || angle < -157.5) {
|
||||
direction = 'left'; // 左
|
||||
} else if (angle >= -157.5 && angle < -112.5) {
|
||||
direction = 'forward_left'; // 左前
|
||||
} else if (angle >= -112.5 && angle < -67.5) {
|
||||
direction = 'forward'; // 前
|
||||
} else if (angle >= -67.5 && angle < -22.5) {
|
||||
direction = 'forward_right'; // 右前
|
||||
}
|
||||
}
|
||||
|
||||
// 处理旋转按钮 (X键和Y键)
|
||||
if (buttons[2].pressed) { // X键 - 左旋转
|
||||
direction = 'rotate_left';
|
||||
} else if (buttons[3].pressed) { // Y键 - 右旋转
|
||||
direction = 'rotate_right';
|
||||
}
|
||||
|
||||
// 处理录制按钮 (十字键上键) - 修复:检测按键按下事件
|
||||
if (buttons[12].pressed) { // 十字键上键 - 录制
|
||||
if (!recordButtonPressed) {
|
||||
recordButtonPressed = true;
|
||||
if (!isPlaying) {
|
||||
if (isRecording) {
|
||||
// 停止录制
|
||||
isRecording = false;
|
||||
status.innerHTML = `<p>录制完成,共记录 ${recordedActions.length} 个操作</p>`;
|
||||
console.log('Recording stopped, actions:', recordedActions);
|
||||
} else {
|
||||
// 开始录制
|
||||
isRecording = true;
|
||||
recordedActions = [];
|
||||
recordingStartTime = Date.now();
|
||||
status.innerHTML = '<p>开始录制...</p>';
|
||||
console.log('Recording started');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
recordButtonPressed = false;
|
||||
}
|
||||
|
||||
// 处理回放按钮 (十字键左键) - 修复:检测按键按下事件
|
||||
if (buttons[14].pressed) { // 十字键左键 - 回放
|
||||
if (!playbackButtonPressed) {
|
||||
playbackButtonPressed = true;
|
||||
if (!isRecording && !isPlaying && recordedActions.length > 0) {
|
||||
isPlaying = true;
|
||||
status.innerHTML = '<p>开始回放...</p>';
|
||||
console.log('Starting playback');
|
||||
|
||||
// 按时间顺序回放操作
|
||||
let currentTime = 0;
|
||||
const totalDuration = recordedActions[recordedActions.length - 1].timestamp;
|
||||
|
||||
recordedActions.forEach((action, index) => {
|
||||
if (index === 0) {
|
||||
// 第一个操作立即执行
|
||||
console.log('Executing action immediately:', action);
|
||||
controlMotor(action.direction);
|
||||
} else {
|
||||
// 计算与前一个操作的时间差
|
||||
const prevAction = recordedActions[index - 1];
|
||||
const delay = action.timestamp - prevAction.timestamp;
|
||||
currentTime += delay;
|
||||
|
||||
console.log('Scheduling action:', action, 'at delay:', currentTime);
|
||||
setTimeout(() => {
|
||||
console.log('Executing action:', action);
|
||||
controlMotor(action.direction);
|
||||
}, currentTime);
|
||||
}
|
||||
});
|
||||
|
||||
// 回放完成后,确保发送停止请求
|
||||
setTimeout(() => {
|
||||
console.log('Playback completed, sending stop command');
|
||||
controlMotor('stop');
|
||||
status.innerHTML = '<p>回放完成</p>';
|
||||
console.log('Playback completed');
|
||||
isPlaying = false;
|
||||
}, totalDuration + 2000);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
playbackButtonPressed = false;
|
||||
}
|
||||
|
||||
// 如果方向改变,发送控制命令
|
||||
if (direction !== lastDirection) {
|
||||
// 如果正在录制,记录操作
|
||||
if (isRecording) {
|
||||
const action = {
|
||||
direction: direction,
|
||||
timestamp: Date.now() - recordingStartTime
|
||||
};
|
||||
recordedActions.push(action);
|
||||
}
|
||||
controlMotor(direction);
|
||||
lastDirection = direction;
|
||||
}
|
||||
|
||||
// 继续监听
|
||||
requestAnimationFrame(updateGamepad);
|
||||
}
|
||||
|
||||
// 控制电机的函数(与control.js中的相同)
|
||||
function controlMotor(direction) {
|
||||
// 根据方向设置状态消息
|
||||
if (direction === 'stop') {
|
||||
status.innerHTML = `<p>正在停止...</p>`;
|
||||
} else {
|
||||
let actionText = '';
|
||||
switch (direction) {
|
||||
case 'forward': actionText = '前进'; break;
|
||||
case 'backward': actionText = '后退'; break;
|
||||
case 'left': actionText = '左移'; break;
|
||||
case 'right': actionText = '右移'; break;
|
||||
case 'rotate_left': actionText = '左旋转'; break;
|
||||
case 'rotate_right': actionText = '右旋转'; break;
|
||||
case 'forward_left': actionText = '左前移动'; break;
|
||||
case 'forward_right': actionText = '右前移动'; break;
|
||||
case 'backward_left': actionText = '左后移动'; break;
|
||||
case 'backward_right': actionText = '右后移动'; break;
|
||||
default: actionText = '移动'; break;
|
||||
}
|
||||
status.innerHTML = `<p>正在${actionText}...</p>`;
|
||||
}
|
||||
|
||||
// 记录操作(如果正在录制)
|
||||
if (isRecording) {
|
||||
const timestamp = Date.now() - recordingStartTime;
|
||||
// 注意:这里不再重复记录,因为已经在updateGamepad中记录了
|
||||
console.log('Recorded action:', { direction, timestamp });
|
||||
} else {
|
||||
console.log('Not recording, action:', direction);
|
||||
}
|
||||
|
||||
// 发送AJAX请求到后端服务器
|
||||
fetch('/control', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ direction: direction })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
status.innerHTML = `<p>${data.message}</p>`;
|
||||
} else {
|
||||
status.innerHTML = `<p>错误: ${data.message}</p>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
status.innerHTML = `<p>通信错误: ${error.message}</p>`;
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
status.innerHTML = '<p>请连接游戏手柄...</p>';
|
||||
console.log('Joycon.js initialized');
|
||||
});
|
||||
@ -1,12 +1,25 @@
|
||||
// 获取按钮和状态元素
|
||||
const forwardBtn = document.getElementById('forwardBtn');
|
||||
const backwardBtn = document.getElementById('backwardBtn');
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
const playbackBtn = document.getElementById('playbackBtn');
|
||||
const status = document.getElementById('status');
|
||||
|
||||
// 录制相关变量
|
||||
let isRecording = false;
|
||||
let recordedActions = [];
|
||||
let recordingStartTime = 0;
|
||||
|
||||
// 实际控制函数
|
||||
function controlMotor(direction) {
|
||||
status.innerHTML = `<p>正在${direction === 'forward' ? '前进' : '后退'}...</p>`;
|
||||
|
||||
// 记录操作(如果正在录制)
|
||||
if (isRecording) {
|
||||
const timestamp = Date.now() - recordingStartTime;
|
||||
recordedActions.push({ direction, timestamp });
|
||||
}
|
||||
|
||||
// 发送AJAX请求到后端服务器
|
||||
fetch('/control', {
|
||||
method: 'POST',
|
||||
@ -28,6 +41,50 @@ function controlMotor(direction) {
|
||||
});
|
||||
}
|
||||
|
||||
// 录制按钮点击事件
|
||||
recordBtn.addEventListener('click', () => {
|
||||
if (isRecording) {
|
||||
// 停止录制
|
||||
isRecording = false;
|
||||
recordBtn.textContent = '开始录制';
|
||||
status.innerHTML = `<p>录制完成,共记录 ${recordedActions.length} 个操作</p>`;
|
||||
} else {
|
||||
// 开始录制
|
||||
isRecording = true;
|
||||
recordedActions = [];
|
||||
recordingStartTime = Date.now();
|
||||
recordBtn.textContent = '停止录制';
|
||||
status.innerHTML = `<p>开始录制...</p>`;
|
||||
}
|
||||
});
|
||||
|
||||
// 回放按钮点击事件
|
||||
playbackBtn.addEventListener('click', () => {
|
||||
if (recordedActions.length === 0) {
|
||||
status.innerHTML = `<p>没有可回放的操作</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
status.innerHTML = `<p>开始回放...</p>`;
|
||||
|
||||
// 按时间顺序回放操作
|
||||
let lastTimestamp = 0;
|
||||
recordedActions.forEach((action, index) => {
|
||||
setTimeout(() => {
|
||||
controlMotor(action.direction);
|
||||
|
||||
// 最后一个操作完成后显示回放完成
|
||||
if (index === recordedActions.length - 1) {
|
||||
setTimeout(() => {
|
||||
status.innerHTML = `<p>回放完成</p>`;
|
||||
}, 2000);
|
||||
}
|
||||
}, action.timestamp - lastTimestamp);
|
||||
|
||||
lastTimestamp = action.timestamp;
|
||||
});
|
||||
});
|
||||
|
||||
// 按钮点击事件
|
||||
forwardBtn.addEventListener('click', () => {
|
||||
controlMotor('forward');
|
||||
|
||||
Binary file not shown.
BIN
main/__pycache__/motor.cpython-38.pyc
Normal file
BIN
main/__pycache__/motor.cpython-38.pyc
Normal file
Binary file not shown.
278
main/camera_test.py
Normal file
278
main/camera_test.py
Normal file
@ -0,0 +1,278 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
|
||||
# 信号处理函数,用于捕获Ctrl+C
|
||||
def signal_handler(sig, frame):
|
||||
print("\n接收到中断信号,程序退出")
|
||||
sys.exit(0)
|
||||
|
||||
# 注册信号处理函数
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
print("开始运行摄像头测试程序...")
|
||||
print(f"OpenCV版本: {cv2.__version__}")
|
||||
|
||||
# 尝试打开摄像头
|
||||
cap = cv2.VideoCapture(0)
|
||||
|
||||
if not cap.isOpened():
|
||||
print("无法打开摄像头!")
|
||||
exit()
|
||||
|
||||
print("摄像头打开成功")
|
||||
|
||||
# 设置摄像头参数
|
||||
cap.set(3, 320)
|
||||
cap.set(4, 240)
|
||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
print(f"摄像头分辨率: {cap.get(3)}x{cap.get(4)}")
|
||||
|
||||
# 初始化稳定阶段
|
||||
print("初始化摄像头...")
|
||||
success_count = 0
|
||||
for i in range(10):
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print(f"第{i+1}帧读取失败")
|
||||
continue
|
||||
success_count += 1
|
||||
print(f"第{i+1}帧读取成功")
|
||||
time.sleep(0.1)
|
||||
print(f"摄像头初始化完成,成功读取{success_count}帧")
|
||||
|
||||
# 读取初始帧
|
||||
ret, prev_frame = cap.read()
|
||||
if not ret:
|
||||
print("无法读取初始帧!")
|
||||
cap.release()
|
||||
exit()
|
||||
|
||||
print("初始帧读取成功")
|
||||
prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
|
||||
prev_gray = cv2.GaussianBlur(prev_gray, (5,5), 0)
|
||||
print("初始帧处理完成")
|
||||
|
||||
# 记录上一帧目标的位置和面积
|
||||
prev_cx = None
|
||||
prev_cy = None
|
||||
prev_area = None
|
||||
# 目标跟踪状态
|
||||
tracking_target = None
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
gray = cv2.GaussianBlur(gray, (5,5), 0)
|
||||
|
||||
# 帧差
|
||||
diff = cv2.absdiff(prev_gray, gray)
|
||||
diff = cv2.convertScaleAbs(diff, alpha=2.5)
|
||||
|
||||
# 应用形态学操作,去除噪声
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
||||
diff = cv2.morphologyEx(diff, cv2.MORPH_OPEN, kernel)
|
||||
diff = cv2.morphologyEx(diff, cv2.MORPH_CLOSE, kernel)
|
||||
|
||||
_, thresh = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)
|
||||
|
||||
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
# ===== 核心:目标检测和跟踪 =====
|
||||
target = None
|
||||
max_area = 0
|
||||
|
||||
# 找到所有符合条件的轮廓
|
||||
valid_contours = []
|
||||
for cnt in contours:
|
||||
area = cv2.contourArea(cnt)
|
||||
|
||||
if area < 200: # 过滤噪声,增大阈值
|
||||
continue
|
||||
|
||||
if area > 8000: # 过滤大面积假目标(如整个画面),减小阈值
|
||||
continue
|
||||
|
||||
# 计算轮廓的宽高比,过滤不符合垃圾特征的目标
|
||||
x, y, w, h = cv2.boundingRect(cnt)
|
||||
aspect_ratio = float(w) / h if h > 0 else 0
|
||||
if aspect_ratio > 3 or aspect_ratio < 0.3: # 过滤过于狭长的目标
|
||||
continue
|
||||
|
||||
valid_contours.append((area, cnt, x, y, w, h))
|
||||
|
||||
# 如果有有效的轮廓
|
||||
if valid_contours:
|
||||
# 如果正在跟踪目标,优先选择与上一目标位置接近的轮廓
|
||||
if tracking_target is not None and prev_cx is not None and prev_cy is not None:
|
||||
best_match = None
|
||||
min_distance = float('inf')
|
||||
|
||||
search_radius = 80 # 目标锁定区域半径
|
||||
|
||||
for area, cnt, x, y, w, h in valid_contours:
|
||||
cx = x + w // 2
|
||||
cy = y + h // 2
|
||||
# 计算与上一目标的距离
|
||||
distance = ((cx - prev_cx) ** 2 + (cy - prev_cy) ** 2) ** 0.5
|
||||
|
||||
# 只考虑在锁定区域内的目标
|
||||
if distance > search_radius:
|
||||
continue
|
||||
|
||||
# 如果距离小于阈值,认为是同一个目标
|
||||
if distance < 50: # 距离阈值
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
best_match = (area, cnt, x, y, w, h)
|
||||
|
||||
# 如果找到匹配的目标
|
||||
if best_match:
|
||||
area, cnt, x, y, w, h = best_match
|
||||
target = cnt
|
||||
max_area = area
|
||||
else:
|
||||
# 如果没有找到匹配的目标,选择最大的轮廓
|
||||
valid_contours.sort(key=lambda x: x[0], reverse=True)
|
||||
area, cnt, x, y, w, h = valid_contours[0]
|
||||
target = cnt
|
||||
max_area = area
|
||||
# 开始跟踪新目标
|
||||
tracking_target = True
|
||||
else:
|
||||
# 如果没有正在跟踪的目标,选择最大的轮廓
|
||||
valid_contours.sort(key=lambda x: x[0], reverse=True)
|
||||
area, cnt, x, y, w, h = valid_contours[0]
|
||||
target = cnt
|
||||
max_area = area
|
||||
# 开始跟踪新目标
|
||||
tracking_target = True
|
||||
|
||||
# ===== 只处理一个目标 =====
|
||||
if target is not None:
|
||||
x, y, w, h = cv2.boundingRect(target)
|
||||
cx = x + w // 2
|
||||
cy = y + h // 2
|
||||
|
||||
# 目标位置平滑处理
|
||||
alpha = 0.6
|
||||
if prev_cx is not None and prev_cy is not None:
|
||||
cx = int(alpha * prev_cx + (1 - alpha) * cx)
|
||||
cy = int(alpha * prev_cy + (1 - alpha) * cy)
|
||||
|
||||
# 绘制画面中心
|
||||
center_x = 160 # 画面中心x坐标
|
||||
center_y = 120 # 画面中心y坐标
|
||||
cv2.circle(frame, (center_x, center_y), 5, (255, 0, 0), -1)
|
||||
|
||||
# 绘制目标矩形和中心
|
||||
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
|
||||
cv2.circle(frame, (cx, cy), 5, (0, 0, 255), -1)
|
||||
|
||||
print("唯一目标:", cx, cy, "area:", max_area)
|
||||
|
||||
# 检测目标是否突然跳变
|
||||
target_jumped = False
|
||||
if prev_cx is not None and prev_cy is not None and prev_area is not None:
|
||||
# 计算位置变化
|
||||
dx = abs(cx - prev_cx)
|
||||
dy = abs(cy - prev_cy)
|
||||
# 计算面积变化比例
|
||||
area_ratio = max_area / prev_area if prev_area > 0 else 0
|
||||
|
||||
# 如果位置变化过大或面积变化过大,认为目标跳变
|
||||
if dx > 100 or dy > 100 or area_ratio < 0.3 or area_ratio > 3:
|
||||
target_jumped = True
|
||||
print("目标突然跳变,可能已离开视野范围")
|
||||
# 重置跟踪状态
|
||||
tracking_target = None
|
||||
|
||||
# 控制逻辑:根据x轴和y轴坐标控制垃圾桶移动
|
||||
center_x = 160 # 画面中心x坐标
|
||||
center_y = 120 # 画面中心y坐标
|
||||
threshold = 20 # 阈值范围
|
||||
|
||||
if target_jumped:
|
||||
# 目标跳变,停止移动
|
||||
print("目标跳变,停止移动")
|
||||
# motor.stop()
|
||||
else:
|
||||
# 计算目标与中心的偏差
|
||||
dx = cx - center_x
|
||||
dy = cy - center_y
|
||||
|
||||
# 确定移动方向
|
||||
if abs(dx) < threshold and abs(dy) < threshold:
|
||||
# 目标在中心附近,停止移动
|
||||
print("目标在中心,停止移动")
|
||||
# motor.stop()
|
||||
elif abs(dx) < threshold:
|
||||
# 只在y轴方向有偏差
|
||||
if dy > threshold:
|
||||
# 目标在下侧,垃圾桶向前移动
|
||||
print("目标在下侧,向前移动")
|
||||
# motor.forward(speed=0.6)
|
||||
else:
|
||||
# 目标在上侧,垃圾桶向后移动
|
||||
print("目标在上侧,向后移动")
|
||||
# motor.backward(speed=0.6)
|
||||
elif abs(dy) < threshold:
|
||||
# 只在x轴方向有偏差
|
||||
if dx > threshold:
|
||||
# 目标在右侧,垃圾桶向右移动
|
||||
print("目标在右侧,向右移动")
|
||||
# motor.move_right(speed=0.6)
|
||||
else:
|
||||
# 目标在左侧,垃圾桶向左移动
|
||||
print("目标在左侧,向左移动")
|
||||
# motor.move_left(speed=0.6)
|
||||
else:
|
||||
# 在x轴和y轴方向都有偏差,使用斜向移动
|
||||
if dx > threshold and dy > threshold:
|
||||
# 目标在右下侧,垃圾桶向右前移动
|
||||
print("目标在右下侧,向右前移动")
|
||||
# motor.move_right_forward(speed=0.6)
|
||||
elif dx > threshold and dy < -threshold:
|
||||
# 目标在右上侧,垃圾桶向右后移动
|
||||
print("目标在右上侧,向右后移动")
|
||||
# motor.move_right_backward(speed=0.6)
|
||||
elif dx < -threshold and dy > threshold:
|
||||
# 目标在左下侧,垃圾桶向左前移动
|
||||
print("目标在左下侧,向左前移动")
|
||||
# motor.move_left_forward(speed=0.6)
|
||||
else:
|
||||
# 目标在左上侧,垃圾桶向左后移动
|
||||
print("目标在左上侧,向左后移动")
|
||||
# motor.move_left_backward(speed=0.6)
|
||||
|
||||
# 更新上一帧的目标信息
|
||||
prev_cx = cx
|
||||
prev_cy = cy
|
||||
prev_area = max_area
|
||||
else:
|
||||
# 没有检测到目标,停止移动
|
||||
print("未检测到目标,停止移动")
|
||||
# motor.stop()
|
||||
# 重置上一帧的目标信息
|
||||
prev_cx = None
|
||||
prev_cy = None
|
||||
prev_area = None
|
||||
# 重置跟踪状态
|
||||
tracking_target = None
|
||||
|
||||
cv2.imshow("tracking", frame)
|
||||
|
||||
prev_gray = gray.copy()
|
||||
|
||||
if cv2.waitKey(1) & 0xFF == 27:
|
||||
break
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
# motor.stop()
|
||||
@ -13,31 +13,38 @@
|
||||
|
||||
<!-- 手柄风格控制器 -->
|
||||
<div class="controller">
|
||||
<!-- 移动控制区域 -->
|
||||
<div class="movement-controls">
|
||||
<div class="movement-row">
|
||||
<button class="btn btn-forward-left" id="forwardLeftBtn"><span>左前</span></button>
|
||||
<button class="btn btn-forward" id="forwardBtn"><span>前进</span></button>
|
||||
<button class="btn btn-forward-right" id="forwardRightBtn"><span>右前</span></button>
|
||||
<div class="controller-layout">
|
||||
<!-- 左侧:八个方向控制 -->
|
||||
<div class="dpad">
|
||||
<div class="dpad-row">
|
||||
<button class="btn btn-forward-left" id="forwardLeftBtn"><span>↖</span></button>
|
||||
<button class="btn btn-forward" id="forwardBtn"><span>↑</span></button>
|
||||
<button class="btn btn-forward-right" id="forwardRightBtn"><span>↗</span></button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="btn btn-left" id="leftBtn"><span>←</span></button>
|
||||
<div class="btn btn-center"></div>
|
||||
<button class="btn btn-right" id="rightBtn"><span>→</span></button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="btn btn-backward-left" id="backwardLeftBtn"><span>↙</span></button>
|
||||
<button class="btn btn-backward" id="backwardBtn"><span>↓</span></button>
|
||||
<button class="btn btn-backward-right" id="backwardRightBtn"><span>↘</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="movement-row">
|
||||
<button class="btn btn-left" id="leftBtn"><span>左移</span></button>
|
||||
<button class="btn btn-backward" id="backwardBtn"><span>后退</span></button>
|
||||
<button class="btn btn-right" id="rightBtn"><span>右移</span></button>
|
||||
|
||||
<!-- 中间:录制回放控制 -->
|
||||
<div class="center-controls">
|
||||
<button class="btn btn-record" id="recordBtn"><span>录制</span></button>
|
||||
<button class="btn btn-playback" id="playbackBtn"><span>回放</span></button>
|
||||
</div>
|
||||
<div class="movement-row">
|
||||
<button class="btn btn-backward-left" id="backwardLeftBtn"><span>左后</span></button>
|
||||
<button class="btn btn-rotate-left" id="rotateLeftBtn"><span>左旋转</span></button>
|
||||
<button class="btn btn-rotate-right" id="rotateRightBtn"><span>右旋转</span></button>
|
||||
<button class="btn btn-backward-right" id="backwardRightBtn"><span>右后</span></button>
|
||||
|
||||
<!-- 右侧:旋转控制 -->
|
||||
<div class="rotate-controls">
|
||||
<button class="btn btn-rotate-left" id="rotateLeftBtn"><span>↶</span></button>
|
||||
<button class="btn btn-rotate-right" id="rotateRightBtn"><span>↷</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 录制回放控制区域 -->
|
||||
<div class="record-controls">
|
||||
<button class="btn btn-record" id="recordBtn"><span>开始录制</span></button>
|
||||
<button class="btn btn-playback" id="playbackBtn"><span>回放</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态显示区域 -->
|
||||
@ -48,5 +55,6 @@
|
||||
|
||||
<script src="Javascript/control.js"></script>
|
||||
<script src="Javascript/record.js"></script>
|
||||
<script src="Javascript/joycon.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -72,25 +72,25 @@ def motor_drive(name, speed):
|
||||
# ======================
|
||||
# 基础移动
|
||||
# ======================
|
||||
def forward(speed=0.6):
|
||||
def forward(speed=0.8):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", speed)
|
||||
motor_drive("M4", speed)
|
||||
|
||||
def backward(speed=0.6):
|
||||
def backward(speed=0.8):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", -speed)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
def move_left(speed=0.6):
|
||||
def move_left(speed=0.8):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", speed)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
def move_right(speed=0.6):
|
||||
def move_right(speed=0.8):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", -speed)
|
||||
@ -99,13 +99,13 @@ def move_right(speed=0.6):
|
||||
# ======================
|
||||
# 旋转
|
||||
# ======================
|
||||
def rotate_left(speed=0.6):
|
||||
def rotate_left(speed=0.8):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", -speed)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
def rotate_right(speed=0.6):
|
||||
def rotate_right(speed=0.8):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", speed)
|
||||
@ -114,25 +114,25 @@ def rotate_right(speed=0.6):
|
||||
# ======================
|
||||
# 斜向移动
|
||||
# ======================
|
||||
def move_left_forward(speed=0.6):
|
||||
def move_left_forward(speed=0.8):
|
||||
motor_drive("M1", 0)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", 0)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
def move_right_forward(speed=0.6):
|
||||
def move_right_forward(speed=0.8):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", 0)
|
||||
motor_drive("M3", -speed)
|
||||
motor_drive("M4", 0)
|
||||
|
||||
def move_left_backward(speed=0.6):
|
||||
def move_left_backward(speed=0.8):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", 0)
|
||||
motor_drive("M3", speed)
|
||||
motor_drive("M4", 0)
|
||||
|
||||
def move_right_backward(speed=0.6):
|
||||
def move_right_backward(speed=0.8):
|
||||
motor_drive("M1", 0)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", 0)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user