GateWay/src/views/CarControl.vue
2025-03-18 17:32:30 +08:00

705 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="control-panel">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="placeholder-div"></div>
<div class="title-container">
<div class="title-box">
<!-- 根据方向状态显示不同文本 -->
{{ getDirectionText }}
<img src="../assets/img/shout.png" alt="voice" class="voice-icon-img"
:class="{ 'show-voice': isControlPressed }" style="width: 20px; height: 20px; margin-left: 20px;">
</div>
</div>
<div class="top-right-icons">
<img :src="connectionImage" alt="link" class="top-icon">
<!-- 注释掉电池图标 -->
<!-- <img :src="batteryImage" alt="battery" class="top-icon"> -->
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 左侧控制开关 -->
<div class="left-controls">
<div class="switch-item">
<span>屏幕</span>
<el-switch v-model="screenStatus" @change="handleScreenChange" />
</div>
<div class="switch-item">
<span>警灯</span>
<el-switch v-model="warningLightStatus" @change="handleWarningLightChange" />
</div>
<div class="switch-item">
<span>跟随</span>
<el-switch v-model="followStatus" @change="handleFollowChange" />
</div>
<div class="switch-item">
<span>避障</span>
<el-switch v-model="obstacleAvoidEnabled" @change="handleObstacleAvoidChange" />
</div>
</div>
<!-- 中间车辆状态显示区域 -->
<div class="center-display">
<div class="status-text">云台状态</div>
<div class="car-display">
<div class="boom-container">
<img src="../assets/img/boom.png" alt="boom" class="boom-image top-boom" v-show="obstacleStatus.top">
<img src="../assets/img/car.png" alt="car" class="car-image" />
<img src="../assets/img/boom.png" alt="boom" class="boom-image bottom-boom" v-show="obstacleStatus.bottom">
</div>
</div>
</div>
<!-- 右侧状态图标 -->
<div class="right-status">
<div class="status-icons">
<div class="icons-row">
<div class="icon-item">
<img src="../assets/img/refresh.png" alt="refresh" @mousedown="handleRefresh"
style="width: 40px; height: 40px;margin-right: 20px;">
</div>
<div class="icon-item">
<img src="../assets/img/mp3.png" alt="play" @click="goto('voiceset')"
style="width: 40px; height: 40px;margin-left: 20px;">
</div>
</div>
</div>
<div class="control-button">
<div class="circle-border" :class="{ 'pressed': isControlPressed }" @mousedown="handleControlPress"
@mouseup="handleControlRelease" @mouseleave="handleControlRelease" @touchstart="handleControlPress"
@touchend="handleControlRelease" @touchcancel="handleControlRelease">
<img src="../assets/img/stop.png" alt="control" class="control-button-img"
style="width: 80px; height: 80px;">
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { useRouter } from 'vue-router';
import GamepadController from '../assets/js/GamepadController';
import audioTransmitter from '../assets/js/audio-transmitter';
export default {
name: 'CarControl',
data() {
return {
screenStatus: false,
warningLightStatus: false,
followStatus: false,
obstacleStatus: {
top: false,
bottom: false
},
obstacleAvoidEnabled: false,
gamepadController: null,
directionAxis0_1: "",
directionAxis9: "",
isGamepadConnected: false,
isControlPressed: false,
ws: null,
wsConnected: false,
// batteryLevel: 0, // 注释掉电池电量
lastResponseTime: 0,
connectionCheckInterval: null,
sendInterval: null,
lastDirection: 0,
lastSpeed: 0,
platform_fun: 0,
button: "",
audioConfig: audioTransmitter.data(),
isRecording: false,
audioHandler: null,
reconnectTimer: null, // 添加重连定时器引用
isComponentUnmounted: false, // 添加组件卸载标志
isInitialized: false, // 添加初始化标志
}
},
created() {
// 初始化 GamepadController禁用调试模式
this.gamepadController = new GamepadController({
debug: false, // 将 debug 设置为 false
deadZone: 0.1,
updateInterval: 50
});
// 添加全局事件监听
window.addEventListener("gamepadconnected", this.handleGamepadConnected);
window.addEventListener("gamepaddisconnected", this.handleGamepadDisconnected);
this.isComponentUnmounted = false; // 添加组件卸载标志
this.initWebSocket();
// 启动连接状态检查
this.connectionCheckInterval = setInterval(() => {
this.checkConnectionStatus();
}, 3000); // 每3秒检查一次
// 启动定时发送数据
this.sendInterval = setInterval(() => {
this.sendControlData();
}, 400); // 每100ms发送一次
// 初始化音频处理器
this.audioHandler = {
...audioTransmitter.data(),
...audioTransmitter.methods
};
},
beforeDestroy() {
this.isComponentUnmounted = true; // 标记组件已卸载
// 清除重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
// 组件销毁前清理资源
if (this.gamepadController) {
this.gamepadController.destroy();
}
// 移除全局事件监听
window.removeEventListener("gamepadconnected", this.handleGamepadConnected);
window.removeEventListener("gamepaddisconnected", this.handleGamepadDisconnected);
if (this.ws) {
this.ws.close();
this.ws = null;
}
if (this.connectionCheckInterval) {
clearInterval(this.connectionCheckInterval);
}
if (this.sendInterval) {
clearInterval(this.sendInterval);
}
// 确保停止音频传输
if (this.audioHandler && this.audioHandler.isRecording) {
this.audioHandler.stopRecording();
}
},
methods: {
goto(path) {
this.$router.push({ name: path });
},
handleGamepadConnected(e) {
console.log('Gamepad connected:', e.gamepad);
this.isGamepadConnected = true;
},
handleGamepadDisconnected() {
console.log('Gamepad disconnected');
this.isGamepadConnected = false;
// 可以在这里添加断开连接的处理逻辑
},
// 获取手柄数据的方法
getGamepadData() {
if (this.gamepadController) {
this.directionAxis0_1 = this.gamepadController.directionAxis0_1;
this.directionAxis9 = this.gamepadController.directionAxis9;
// 可以在这里添加其他数据的获取和处理
}
},
handleControlPress() {
this.isControlPressed = true;
// 开始音频传输
console.log('开始音频传输...');
if (this.audioHandler) {
this.audioHandler.startRecording();
}
},
handleControlRelease() {
this.isControlPressed = false;
// 停止音频传输
console.log('停止音频传输...');
if (this.audioHandler) {
this.audioHandler.stopRecording();
}
},
initWebSocket() {
try {
console.log('正在连接 WebSocket...');
this.ws = new WebSocket('ws://192.168.4.120/ws');
// this.ws = new WebSocket('ws://192.168.1.120/ws');
// this.ws = new WebSocket('ws://192.168.1.60:81');
this.ws.onopen = () => {
console.log('WebSocket 已连接');
this.wsConnected = true;
this.lastResponseTime = Date.now();
this.sendCarInfo();
};
this.ws.onmessage = (event) => {
this.lastResponseTime = Date.now();
try {
const response = JSON.parse(event.data);
console.log('收到数据:', JSON.stringify(response));
if (response.JSON_id === 1 && response.rc_car_card) {
this.updateCarStatus(response.rc_car_card);
}
} catch (error) {
console.error('解析响应数据失败:', error);
}
};
this.ws.onclose = () => {
console.log('WebSocket连接关闭');
// 只有在组件没有被卸载的情况下才重连
if (!this.isComponentUnmounted) {
this.reconnectTimer = setTimeout(() => {
this.initWebSocket();
}, 3000);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
this.wsConnected = false;
// 打印更详细的错误信息
if (error.message) {
console.error('错误信息:', error.message);
}
};
} catch (error) {
console.error('WebSocket 初始化失败:', error);
// 尝试重新连接
setTimeout(() => {
console.log('尝试重新连接...');
this.initWebSocket();
}, 3000);
}
},
// 修改 updateGamepadData 方法
updateGamepadData() {
if (this.gamepadController) {
// 获取手柄数据
const angle = this.gamepadController.angle;
const directionAxis9 = this.gamepadController.directionAxis9;
const speed = this.gamepadController.speed;
// 根据角度计算方向值0-4
let direction = 0; // 默认停止
if (speed > 10) { // 添加速度阈值,避免小幅度移动
if (angle >= 315 || angle < 45) {
direction = 4; // 右转
} else if (angle >= 45 && angle < 135) {
direction = 2; // 后退
} else if (angle >= 135 && angle < 225) {
direction = 3; // 左转
} else if (angle >= 225 && angle < 315) {
direction = 1; // 前进
}
}
// 更新控制按钮状态
const wasPressed = this.isControlPressed;
this.isControlPressed = this.gamepadController.rightJoystickPressed;
// 检测按钮状态变化并处理音频传输
if (!wasPressed && this.isControlPressed) {
// 按钮刚被按下
console.log('手柄按钮按下,开始音频传输...');
if (this.audioHandler) {
this.audioHandler.startRecording();
}
} else if (wasPressed && !this.isControlPressed) {
// 按钮刚被释放
console.log('手柄按钮释放,停止音频传输...');
if (this.audioHandler) {
this.audioHandler.stopRecording();
}
}
// 更新手柄相关状态
this.lastDirection = direction;
if (directionAxis9 !== undefined) this.platform_fun = directionAxis9;
if (speed !== undefined) this.lastSpeed = speed;
}
},
sendCarInfo() {
const carData = {
"JSON_id": 1,
"rc_car_card": {
"get_info": 1,
"get_attribute": 1,
}
};
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(carData));
// 在控制台打印发送的数据
console.log('发送的数据:', JSON.stringify(carData));
} else {
console.error('WebSocket 未连接');
}
},
// 修改 refresh 图标的点击事件
handleRefresh() {
this.sendCarInfo();
},
// 修改状态更新函数
updateCarStatus(data) {
if (data.attribute) {
// 更新障碍物显示状态
this.obstacleStatus = {
top: data.attribute.obstacle_sta === 1,
bottom: data.attribute.obstacle_sta === 2
};
// 只在初始化时更新开关状态
if (!this.isInitialized) {
this.screenStatus = data.attribute.screen_en === 1;
this.warningLightStatus = data.attribute.warn_light_en === 1;
this.followStatus = data.attribute.follow_en === 1;
this.obstacleAvoidEnabled = data.attribute.obstacle_avoid_en === 1;
this.isInitialized = true;
}
}
// 更新方向状态
if (data.driver && typeof data.driver.direction !== 'undefined') {
this.lastDirection = data.driver.direction; // 使用返回的方向值
}
},
checkConnectionStatus() {
const now = Date.now();
// 如果超过5秒没有收到响应认为连接已断开
if (now - this.lastResponseTime > 5000) {
this.wsConnected = false;
}
},
// 修改开关状态处理函数
handleScreenChange(value) {
this.screenStatus = value;
this.sendControlData();
},
handleWarningLightChange(value) {
this.warningLightStatus = value;
this.sendControlData();
},
handleFollowChange(value) {
this.followStatus = value;
this.sendControlData();
},
handleObstacleAvoidChange(value) {
this.obstacleAvoidEnabled = value;
this.sendControlData();
},
// 修改 sendControlData 方法
sendControlData() {
// 只在有手柄数据时更新手柄相关状态
if (this.gamepadController && this.gamepadController.isActive) {
this.updateGamepadData();
}
const controlData = {
"JSON_id": 1,
"rc_car_card": {
"attribute": {
"platform": this.platform_fun,
"screen_en": this.screenStatus ? 1 : 0,
"warn_light_en": this.warningLightStatus ? 1 : 0,
"follow_en": this.followStatus ? 1 : 0,
"obstacle_avoid_en": this.obstacleAvoidEnabled ? 1 : 0,
"car_speed": this.lastSpeed,
"car_direction": this.lastDirection
}
}
};
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(controlData));
console.log('发送控制数据:', JSON.stringify(controlData));
}
},
},
computed: {
// 注释掉电池图片计算属性
/*
batteryImage() {
if (this.batteryLevel >= 80) return require('../assets/img/bat_full.png');
if (this.batteryLevel >= 60) return require('../assets/img/bat_high.png');
if (this.batteryLevel >= 40) return require('../assets/img/bat_medium.png');
if (this.batteryLevel >= 20) return require('../assets/img/bat_low.png');
return require('../assets/img/bat_empty.png');
},
*/
connectionImage() {
return this.wsConnected ?
require('../assets/img/connect.png') :
require('../assets/img/no_connect.png');
},
getDirectionText() {
// 根据 direction 值显示对应状态
switch (this.lastDirection) {
case 0:
return '遥控车已停止';
case 1:
return '遥控车正在前进';
case 2:
return '遥控车正在后退';
case 3:
return '遥控车正在左转';
case 4:
return '遥控车正在右转';
default:
return '遥控车当前状态';
}
}
},
setup() {
const router = useRouter();
// 添加返回按钮处理函数
const onClickLeft = () => {
router.back();
};
return {
onClickLeft,
};
}
}
</script>
<style scoped>
.control-panel {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #000033;
color: #fff;
overflow: hidden;
display: flex;
flex-direction: column;
}
.status-bar {
height: 60px;
background-color: #000033;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
position: relative;
}
.placeholder-div {
width: 63px;
}
.title-container {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 24px;
display: flex;
align-items: center;
gap: 10px;
}
.title-box {
border: 2px solid #00ffff;
padding: 2px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
min-width: 180px; /* 确保文本变化时盒子大小稳定 */
}
.main-content {
flex: 1;
display: flex;
padding: 10px;
gap: 20px;
}
.left-controls {
width: 120px;
display: flex;
flex-direction: column;
gap: 15px;
}
.switch-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border: 1px solid #00ffff;
border-radius: 8px;
font-size: 20px;
}
.center-display {
flex: 1;
border: 1px solid #00ffff;
border-radius: 8px;
padding: 5px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.status-text {
font-size: 20px;
color: #00ffff;
align-self: flex-start;
margin-bottom: 10px;
}
.car-display {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.boom-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
/* gap: 10px; */
}
.boom-image {
width: 60px;
height: 60px;
}
.top-boom {
transform: rotate(0);
}
.bottom-boom {
transform: rotate(180deg);
}
.car-image {
width: 160px;
height: auto;
}
.right-status {
width: 150px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.status-icons {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 50px;
}
.icons-row {
display: flex;
flex-direction: row;
gap: 15px;
justify-content: center;
}
.icon-item {
width: 40px;
height: 40px;
/* border: 1px solid #00ffff; */
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #00ffff;
}
.battery {
background-color: #00ffff;
color: #000033;
}
:deep(.el-switch__core) {
border-color: #00ffff;
background-color: transparent;
}
:deep(.el-switch.is-checked .el-switch__core) {
background-color: #00ffff;
}
.top-right-icons {
display: flex;
gap: 15px;
align-items: center;
width: 63px;
}
.top-icon {
width: 24px;
height: 24px;
cursor: pointer;
}
.circle-border {
width: 100px;
height: 100px;
border: 2px solid white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
transition: border-color 0.3s ease;
cursor: pointer;
}
.circle-border.pressed {
border-color: red;
}
.control-button-img {
width: 80px;
height: 80px;
user-select: none;
/* 防止拖动图片 */
}
.voice-icon-img {
opacity: 0;
transition: opacity 0.3s ease;
}
.voice-icon-img.show-voice {
opacity: 1;
}
/* 确保标题栏在最上层 */
:deep(.van-nav-bar) {
z-index: 999;
}
</style>