This commit is contained in:
JayJiaJun 2025-02-19 07:46:51 +08:00
parent 55833cbdc9
commit 960a08f25d
8 changed files with 791 additions and 246 deletions

40
public/audio-processor.js Normal file
View File

@ -0,0 +1,40 @@
class AudioProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.desiredBufferSize = 20*1024; // 期望的缓冲区大小
this.buffer = new Float32Array(this.desiredBufferSize);
this.bufferIndex = 0;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const channel = input[0];
if (channel && channel.length > 0) {
// 将新数据添加到缓冲区
for (let i = 0; i < channel.length; i++) {
this.buffer[this.bufferIndex++] = channel[i];
// 当缓冲区满时,发送数据
if (this.bufferIndex >= this.desiredBufferSize) {
// 将浮点音频数据转换为16位整数
const pcmData = new Int16Array(this.desiredBufferSize);
for (let j = 0; j < this.desiredBufferSize; j++) {
pcmData[j] = Math.min(Math.max(this.buffer[j] * 32767, -32767), 32767);
}
// 发送数据到主线程
this.port.postMessage(pcmData.buffer, [pcmData.buffer]);
// 重置缓冲区
this.buffer = new Float32Array(this.desiredBufferSize);
this.bufferIndex = 0;
}
}
}
return true;
}
}
registerProcessor('audio-processor', AudioProcessor);

View File

@ -1,84 +1,137 @@
class GamepadController {
constructor(config = {}) {
this.config = {
buttonMapping: {},
axisMapping: {},
...config
constructor(options = {}) {
this.options = {
...{
debug: false,
deadZone: 0.1,
updateInterval: 50,
buttonsConfig: [
{ name: "Left2", index: 6 },
{ name: "Back", index: 8 },
{ name: "Right Joystick Press", index: 11 }
]
},
...options
};
this.gamepadIndex = null;
this.gamepad = null;
this.listeners = {};
this.interval = null;
this.buttons = [];
this.directionAxis0_1 = "";
this.directionAxis9 = "";
this.angle = 0;
// 初始化按钮状态
this.buttons = this.options.buttonsConfig.map(button => ({
...button,
pressed: false
}));
// 初始化默认按键映射
Object.assign(this.config.buttonMapping, {
0: 'A',
1: 'B',
2: 'X',
3: 'Y',
// 其他默认映射
});
// 注册事件监听器
window.addEventListener("gamepadconnected", this.onGamepadConnected.bind(this));
window.addEventListener("gamepaddisconnected", this.onGamepadDisconnected.bind(this));
// 初始化默认摇杆映射
Object.assign(this.config.axisMapping, {
0: 'leftX',
1: 'leftY',
2: 'rightX',
3: 'rightY',
// 其他默认映射
});
if (this.options.debug) {
console.log("GamepadController initialized with options:", this.options);
}
}
connect() {
window.addEventListener('gamepadconnected', (e) => {
this.gamepad = navigator.getGamepads()[e.gamepad.index];
this.triggerEvent('connected');
});
onGamepadConnected(e) {
console.log("Gamepad connected:", e.gamepad);
this.gamepadIndex = e.gamepad.index;
this.gamepad = navigator.getGamepads()[this.gamepadIndex];
this.startGamepad();
}
disconnect() {
window.removeEventListener('gamepadconnected', this.handleConnect);
onGamepadDisconnected() {
clearInterval(this.interval);
this.gamepad = null;
this.triggerEvent('disconnected');
if (this.options.debug) {
console.log("Gamepad disconnected");
}
}
// 手动获取手柄数据的方法
pollGamepad() {
if (!this.gamepad) return;
// 处理摇杆数据
const axesData = this.gamepad.axes;
const axisEvents = {};
Object.entries(this.config.axisMapping).forEach(([index, name]) => {
axisEvents[name] = axesData[index];
});
this.triggerEvent('axisChange', axisEvents);
// 处理按键事件
const buttons = this.gamepad.buttons;
buttons.forEach((btn, index) => {
if (btn.value === 1 && this.config.buttonMapping[index]) {
const keyName = this.config.buttonMapping[index];
this.triggerEvent('buttonPress', { keyName, index });
startGamepad() {
this.interval = setInterval(() => {
const gamepads = navigator.getGamepads();
const gamepad = gamepads[this.gamepadIndex];
if (gamepad) {
// 注释掉调试打印
// if (this.options.debug) {
// console.log('Axes data:', {
// axis0: gamepad.axes[0],
// axis1: gamepad.axes[1],
// gamepadIndex: this.gamepadIndex
// });
// }
this.updateDirection(gamepad.axes);
this.updateDirectionAxis9(gamepad.axes);
this.pressKey(gamepad.buttons);
}
}, this.options.updateInterval);
}
updateDirection(axes) {
const axis0 = axes[0];
const axis1 = axes[1];
// 检查是否在死区
if (Math.abs(axis0) < this.options.deadZone && Math.abs(axis1) < this.options.deadZone) {
this.directionAxis0_1 = "未定义";
this.angle = 0; // 在死区时重置角度为0
return;
}
// 计算方向角度0-360度
let angle = Math.atan2(axis1, axis0) * (180 / Math.PI);
angle = (angle + 360) % 360; // 确保角度在 0-360 范围内
angle = Math.round(angle);
this.angle = angle;
// 更新方向数据
if (Math.abs(axis0) > this.options.deadZone || Math.abs(axis1) > this.options.deadZone) {
this.directionAxis0_1 = `${angle}°`;
// 注释掉调试打印
// if (this.options.debug) {
// console.log(` 摇杆方向: ${angle}°, X轴: ${axis0.toFixed(2)}, Y轴: ${axis1.toFixed(2)}`);
// }
}
}
updateDirectionAxis9(axes) {
const axis9 = axes[9];
const roundedAxis9 = Math.round(axis9 * 100) / 100;
if (roundedAxis9 <= -0.9) {
this.directionAxis9 = "上";
} else if (roundedAxis9 >= 0.0 && roundedAxis9 <= 0.2) {
this.directionAxis9 = "下";
} else if (roundedAxis9 >= 0.6 && roundedAxis9 <= 0.8) {
this.directionAxis9 = "左";
} else if (roundedAxis9 >= -0.5 && roundedAxis9 <= -0.4) {
this.directionAxis9 = "右";
} else {
this.directionAxis9 = "未定义";
}
}
pressKey(buttons) {
this.buttons.forEach(button => {
const buttonData = buttons[button.index];
button.pressed = buttonData ? buttonData.value === 1 : false;
});
}
addEventListener(type, callback) {
if (!this.listeners[type]) this.listeners[type] = [];
this.listeners[type].push(callback);
}
triggerEvent(type, data = {}) {
if (this.listeners[type]) {
this.listeners[type].forEach(callback => callback(data));
destroy() {
clearInterval(this.interval);
window.removeEventListener("gamepadconnected", this.onGamepadConnected);
window.removeEventListener("gamepaddisconnected", this.onGamepadDisconnected);
if (this.options.debug) {
console.log("GamepadController destroyed");
}
}
}
// 导出模块
if (typeof module !== 'undefined' && module.exports) {
module.exports = GamepadController;
} else if (typeof define === 'function' && define.amd) {
define([], () => GamepadController);
} else {
window.GamepadController = GamepadController;
}
// 导出类以便外部使用
export default GamepadController;

View File

@ -1,7 +1,7 @@
import { createRouter, createWebHashHistory } from 'vue-router';
import { useStore } from 'vuex';
import Home from '../views/home/home.vue';
import CarControl from '../views/CarControl.vue'; // 导入小车控制页面
import CarControl from '../views/CarControl.vue'; // 直接导入组件
const routes = [
{
@ -48,9 +48,9 @@ const routes = [
component: () => import('../views/voice/voiceset.vue')
},
{
path: '/car_control', // 添加小车控制的路由
path: '/car_control',
name: 'CarControl',
component: () => import('../views/CarControl.vue')
component: CarControl, // 直接使用导入的组件,而不是使用动态导入
},
{
path: '/audio-play',

View File

@ -5,6 +5,31 @@
{{ isRecording ? '停止传输音频' : '开始传输音频' }}
</button>
<p>{{ status }}</p>
<div class="audio-settings">
<div class="setting-item">
<label>采样率 (Hz):</label>
<select v-model="audioConfig.sampleRate">
<option value="8000">8000</option>
<option value="16000">16000</option>
<option value="44100">44100</option>
<option value="48000">48000</option>
</select>
</div>
<div class="setting-item">
<label>位深度:</label>
<select v-model="audioConfig.bitsPerSample">
<option value="8">8</option>
<option value="16">16</option>
</select>
</div>
<div class="setting-item">
<label>通道数:</label>
<select v-model="audioConfig.channels">
<option value="1">单声道</option>
<option value="2">双声道</option>
</select>
</div>
</div>
</div>
</template>
@ -16,67 +41,109 @@ export default {
mediaRecorder: null, // MediaRecorder
audioChunks: [], //
status: '点击按钮开始传输音频',
isRecording: false //
isRecording: false, //
audioConfig: {
sampleRate: 16000,
bitsPerSample: 16,
channels: 1
},
stream: null,
audioContext: null,
worklet: null,
// lastSendTime: 0,
// sendInterval: 200, // 500ms
};
},
mounted() {
// WebSocket
this.ws = new WebSocket('ws://192.168.1.60:81'); // ESP32 IP
this.ws.onopen = () => {
console.log('WebSocket连接已打开');
this.ws = new WebSocket('ws://192.168.4.103/ws'); // ESP32 IP
// this.ws = new WebSocket('ws://192.168.1.60:81'); // ESP32 IP
this.ws.onopen = () => {
console.log('WebSocket 连接已打开');
};
this.ws.onmessage = (event) => {
console.log('接收到来自ESP32的消息:', event.data);
this.ws.onmessage = (event) => {
console.log(' 接收到来自ESP32的消息:', event.data);
};
this.ws.onclose = () => {
console.log('WebSocket连接已关闭');
this.ws.onclose = () => {
console.log('WebSocket 连接已关闭');
};
},
methods: {
toggleRecording() {
if (this.isRecording) {
if (this.isRecording) {
//
this.stopRecording();
this.stopRecording();
} else {
//
this.startRecording();
this.startRecording();
}
},
startRecording() {
async startRecording() {
this.status = '开始录音并传输音频...';
this.audioChunks = [];
this.isRecording = true;
navigator.mediaDevices.getUserMedia({ audio: true })
.then((stream) => {
this.mediaRecorder = new MediaRecorder(stream);
this.mediaRecorder.ondataavailable = (event) => {
this.audioChunks.push(event.data);
//
const audioBlob = new Blob([event.data], { type: 'audio/wav' });
audioBlob.arrayBuffer().then((arrayBuffer) => {
//
console.log('实时音频数据:', new Uint8Array(arrayBuffer));
// WebSocket
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(arrayBuffer);
}
});
};
this.mediaRecorder.start(100); // 100ms
})
.catch((err) => {
console.error('无法访问麦克风:', err);
const constraints = {
audio: {
sampleRate: parseInt(this.audioConfig.sampleRate),
channelCount: parseInt(this.audioConfig.channels),
sampleSize: parseInt(this.audioConfig.bitsPerSample)
}
};
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
// AudioContext
const audioContext = new AudioContext({
sampleRate: parseInt(this.audioConfig.sampleRate)
});
// AudioWorklet
await audioContext.audioWorklet.addModule('audio-processor.js');
const source = audioContext.createMediaStreamSource(stream);
const worklet = new AudioWorkletNode(audioContext, 'audio-processor');
// worklet
worklet.port.onmessage = (event) => {
if (this.ws.readyState === WebSocket.OPEN) {
// const currentTime = Date.now();
// if (!this.lastSendTime || currentTime - this.lastSendTime > this.sendInterval) {
const pcmData = new Int16Array(event.data);
//
console.log('发送的音频数据:', Array.from(pcmData.slice(0, 20))); // 20
this.ws.send(event.data);
// this.lastSendTime = currentTime;
// }
}
};
source.connect(worklet);
worklet.connect(audioContext.destination);
// 便
this.stream = stream;
this.audioContext = audioContext;
this.worklet = worklet;
} catch (err) {
console.error('无法访问麦克风:', err);
this.status = '访问麦克风失败: ' + err.message;
}
},
stopRecording() {
this.status = '停止录音';
this.isRecording = false;
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
this.mediaRecorder.stop(); //
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
}
if (this.worklet) {
this.worklet.disconnect();
}
if (this.audioContext) {
this.audioContext.close();
}
}
}
@ -89,4 +156,24 @@ button {
font-size: 16px;
cursor: pointer;
}
</style>
.audio-settings {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.setting-item {
margin: 10px 0;
}
.setting-item label {
margin-right: 10px;
}
select {
padding: 5px;
border-radius: 3px;
}
</style>

View File

@ -6,12 +6,13 @@
<div class="title-container">
<div class="title-box">
遥控车当前状态
<img src="../assets/img/shout.png" alt="voice" class="voice-icon-img" style="width: 20px; height: 20px;margin-left: 20px;">
<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="../assets/img/connect.png" alt="link" class="top-icon">
<img src="../assets/img/bat_empty.png" alt="mp3" class="top-icon">
<img :src="connectionImage" alt="link" class="top-icon">
<img :src="batteryImage" alt="battery" class="top-icon">
</div>
</div>
@ -33,7 +34,7 @@
</div>
<div class="switch-item">
<span>避障</span>
<el-switch v-model="obstacleStatus" />
<el-switch v-model="obstacleStatus.top" />
</div>
</div>
@ -41,7 +42,11 @@
<div class="center-display">
<div class="status-text">云台状态</div>
<div class="car-display">
<img src="../assets/img/car.png" alt="car" class="car-image" />
<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>
@ -50,15 +55,22 @@
<div class="status-icons">
<div class="icons-row">
<div class="icon-item">
<img src="../assets/img/refresh.png" alt="refresh" style="width: 40px; height: 40px;">
<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" style="width: 40px; height: 40px;">
<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">
<img src="../assets/img/stop.png" alt="control" class="control-button-img" style="width: 80px; height: 80px;">
<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>
@ -66,6 +78,9 @@
</template>
<script>
import { useRouter } from 'vue-router';
import GamepadController from '../assets/js/GamepadController';
export default {
name: 'CarControl',
data() {
@ -73,7 +88,241 @@ export default {
screenStatus: false,
warningLightStatus: false,
followStatus: false,
obstacleStatus: false
obstacleStatus: {
top: false,
bottom: 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
}
},
created() {
// GamepadController
this.gamepadController = new GamepadController({
debug: true,
deadZone: 0.1,
updateInterval: 50
});
//
window.addEventListener("gamepadconnected", this.handleGamepadConnected);
window.addEventListener("gamepaddisconnected", this.handleGamepadDisconnected);
this.initWebSocket();
//
this.$nextTick(() => {
this.sendCarInfo();
});
//
this.connectionCheckInterval = setInterval(() => {
this.checkConnectionStatus();
}, 3000); // 3
//
this.sendInterval = setInterval(() => {
this.sendControlData();
}, 1000); // 100ms
},
beforeDestroy() {
//
if (this.gamepadController) {
this.gamepadController.destroy();
}
//
window.removeEventListener("gamepadconnected", this.handleGamepadConnected);
window.removeEventListener("gamepaddisconnected", this.handleGamepadDisconnected);
if (this.ws) {
this.ws.close();
}
if (this.connectionCheckInterval) {
clearInterval(this.connectionCheckInterval);
}
if (this.sendInterval) {
clearInterval(this.sendInterval);
}
},
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;
},
handleControlRelease() {
this.isControlPressed = false;
},
initWebSocket() {
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);
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 已断开');
this.wsConnected = false;
};
this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
this.wsConnected = false;
};
},
//
updateGamepadData() {
if (this.gamepadController) {
// angle
const angle = this.gamepadController.angle;
console.log('当前角度:', angle);
this.lastDirection = angle;
//
this.lastSpeed = 0; // 0
}
},
sendCarInfo() {
const carData = {
"JSON_id": 1,
"rc_car_card": {
"get_info": 1,
"get_attribute": 1,
"get_function": 1,
"get_driver": 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.batteryLevel = data.attribute.bat_quantity;
//
this.obstacleStatus = {
top: data.attribute.obstacle_sta === 0,
bottom: data.attribute.obstacle_sta === 1
};
}
if (data.function) {
//
this.screenStatus = data.function.screen_en === 1;
this.warningLightStatus = data.function.warn_light_en === 1;
this.followStatus = data.function.follow_en === 1;
this.obstacleStatus = data.function.obstacle_avoid_en === 1;
}
},
checkConnectionStatus() {
const now = Date.now();
// 5
if (now - this.lastResponseTime > 5000) {
this.wsConnected = false;
}
},
sendControlData() {
//
this.updateGamepadData();
const controlData = {
"JSON_id": 1,
"rc_car_card": {
"get_info": 1,
"get_attribute": 1,
"function": {
"platform_fun": 2,
"screen_en": this.screenStatus ? 1 : 0,
"warn_light_en": this.warningLightStatus ? 1 : 0,
"follow_en": this.followStatus ? 1 : 0,
"obstacle_avoid_en": this.obstacleStatus.top ? 1 : 0
},
"driver": {
"director": this.lastDirection, //
"speed": this.lastSpeed //
}
}
};
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(controlData));
console.log('发送控制数据:', JSON.stringify(controlData));
}
},
},
computed: {
batteryImage() {
// 使 require
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');
}
}
}
@ -119,14 +368,14 @@ export default {
.title-box {
border: 2px solid #00ffff;
padding: 5px 15px;
padding: 2px;
border-radius: 8px;
}
.main-content {
flex: 1;
display: flex;
padding: 20px;
padding: 10px;
gap: 20px;
}
@ -151,7 +400,7 @@ export default {
flex: 1;
border: 1px solid #00ffff;
border-radius: 8px;
padding: 20px;
padding: 5px;
display: flex;
flex-direction: column;
align-items: flex-start;
@ -172,6 +421,27 @@ export default {
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;
@ -237,4 +507,36 @@ export default {
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;
}
</style>

View File

@ -1,45 +1,63 @@
<template>
<div class="home">
<div class="gamepad-container">
<div class="gamepad-info">
<div class="data-card axes-card">
<h3>遥感数据</h3>
<div class="data-display">
<span class="axis-label">X :</span>
<span class="axis-value">{{ directionAxis0_1 }}</span>
<div class="control-panel">
<div class="status-bar">
<div class="placeholder-div"></div>
<div class="title-container">
<div class="title-box">
手柄控制界面
</div>
</div>
<div class="top-right-icons">
<img src="../assets/img/connect.png" alt="link" class="top-icon">
<img src="../assets/img/bat_empty.png" alt="mp3" class="top-icon">
</div>
</div>
<div class="main-content">
<!-- 左侧数据显示 -->
<div class="left-panel">
<div class="data-card">
<div class="card-title">摇杆数据</div>
<div class="data-row">
<span>X轴:</span>
<span class="value">{{ directionAxis0_1 }}</span>
</div>
<div class="data-display">
<span class="axis-label">Y :</span>
<span class="axis-value">{{ directionAxis0_1 }}</span>
<div class="data-row">
<span>Y:</span>
<span class="value">{{ directionAxis0_1 }}</span>
</div>
</div>
<div class="data-card buttons-card">
<h3>按键状态</h3>
<div class="button-group">
<div
v-for="(button, index) in buttons"
:key="index"
class="button-item"
:class="{ 'pressed': button.pressed }"
>
{{ button.name }}
</div>
<!-- 中间按键状态 -->
<div class="center-panel">
<div class="data-card">
<div class="card-title">按键状态</div>
<div class="button-grid">
<div v-for="(button, index) in buttons"
:key="index"
class="button-status"
:class="{ 'active': button.pressed }">
{{ button.name }}
</div>
</div>
</div>
<div class="data-card axis9-card">
<h3>按钮数据Axis9</h3>
<div class="data-display">
<span class="axis-label">方向:</span>
<span class="axis-value">{{ directionAxis9 }}</span>
</div>
<!-- 右侧方向数据 -->
<div class="right-panel">
<div class="data-card">
<div class="card-title">方向数据</div>
<div class="data-row">
<span>当前方向:</span>
<span class="value">{{ directionAxis9 }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Home",
@ -47,6 +65,7 @@ export default {
return {
interval: null,
gamepad: null,
gamepadIndex: null,
buttons: [
{ name: "Left2", pressed: false },
{ name: "Back", pressed: false },
@ -57,65 +76,80 @@ export default {
};
},
created() {
window.addEventListener("gamepadconnected", this.onGamepadConnected);
window.addEventListener("gamepaddisconnected", this.onGamepadDisconnected);
window.addEventListener("gamepadconnected", this.onGamepadConnected);
window.addEventListener("gamepaddisconnected", this.onGamepadDisconnected);
},
beforeDestroy() {
window.removeEventListener("gamepadconnected", this.onGamepadConnected);
window.removeEventListener("gamepaddisconnected", this.onGamepadDisconnected);
window.removeEventListener("gamepadconnected", this.onGamepadConnected);
window.removeEventListener("gamepaddisconnected", this.onGamepadDisconnected);
},
methods: {
onGamepadConnected(e) {
console.log("Gamepad connected:", e.gamepad);
this.gamepad = navigator.getGamepads()[e.gamepad.index];
this.startGamepad();
console.log("Gamepad connected:", e.gamepad);
this.gamepadIndex = e.gamepad.index;
this.gamepad = navigator.getGamepads()[this.gamepadIndex];
this.startGamepad();
},
onGamepadDisconnected() {
clearInterval(this.interval);
this.gamepad = null;
clearInterval(this.interval);
this.gamepad = null;
},
startGamepad() {
this.interval = setInterval(() => {
const gamepad = navigator.getGamepads()[0];
this.interval = setInterval(() => {
const gamepads = navigator.getGamepads();
const gamepad = gamepads[this.gamepadIndex];
if (gamepad) {
this.updateDirection(gamepad.axes);
this.updateDirectionAxis9(gamepad.axes);
this.pressKey(gamepad.buttons);
console.log('Axes data:', {
axis0: gamepad.axes[0],
axis1: gamepad.axes[1],
gamepadIndex: this.gamepadIndex
});
this.updateDirection(gamepad.axes);
this.updateDirectionAxis9(gamepad.axes);
this.pressKey(gamepad.buttons);
}
}, 50);
},
updateDirection(axes) {
const axis0 = axes[0];
const axis1 = axes[1];
if (axis0 >= -0.3 && axis0 <= 0.3 && axis1 <= -0.5) {
this.directionAxis0_1 = "上";
} else if (axis0 >= -0.3 && axis0 <= 0.3 && axis1 >= 0.5) {
this.directionAxis0_1 = "下";
} else if (axis1 >= -0.3 && axis1 <= 0.3 && axis0 <= -0.3) {
this.directionAxis0_1 = "左";
} else if (axis1 >= -0.3 && axis1 <= 0.3 && axis0 >= 0.3) {
this.directionAxis0_1 = "右";
} else {
this.directionAxis0_1 = "未定义";
// 0-360
if (Math.abs(axis0) < 0.1 && Math.abs(axis1) < 0.1) {
this.directionAxis0_1 = "未定义";
return;
}
// 使
let angle = Math.atan2(axis1, axis0) * (180 / Math.PI);
angle = (angle + 360) % 360; // 0-360
angle = Math.round(angle);
//
if (Math.abs(axis0) > 0.1 || Math.abs(axis1) > 0.1) {
this.directionAxis0_1 = angle + "°";
console.log(`摇杆方向: ${angle}°, X轴: ${axis0.toFixed(2)}, Y轴: ${axis1.toFixed(2)}`);
}
},
updateDirectionAxis9(axes) {
const axis9 = axes[9];
const roundedAxis9 = Math.round(axis9 * 100) / 100;
const roundedAxis9 = Math.round(axis9 * 100) / 100;
if (roundedAxis9 <= -0.9) {
this.directionAxis9 = "上";
this.directionAxis9 = "上";
} else if (roundedAxis9 >= 0.0 && roundedAxis9 <= 0.2) {
this.directionAxis9 = "下";
this.directionAxis9 = "下";
} else if (roundedAxis9 >= 0.6 && roundedAxis9 <= 0.8) {
this.directionAxis9 = "左";
this.directionAxis9 = "左";
} else if (roundedAxis9 >= -0.5 && roundedAxis9 <= -0.4) {
this.directionAxis9 = "右";
this.directionAxis9 = "右";
} else {
this.directionAxis9 = "未定义";
this.directionAxis9 = "未定义";
}
},
pressKey(buttons) {
this.buttons = [
this.buttons = [
{ name: "Left2", pressed: buttons[6].value === 1 },
{ name: "Back", pressed: buttons[8].value === 1 },
{ name: "Right Joystick Press", pressed: buttons[11].value === 1 }
@ -124,92 +158,119 @@ export default {
}
};
</script>
<style scoped>
.home {
.control-panel {
position: fixed;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
display: flex;
justify-content: center;
align-items: center;
}
.gamepad-container {
width: 450px;
padding: 2rem;
border-radius: 15px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.gamepad-info {
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #000033;
color: #fff;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 2rem;
}
.data-card {
padding: 1.5rem;
border-radius: 12px;
background: rgba(255, 255, 255, 0.8);
}
h3 {
font-size: 1.4rem;
color: #2c3e50;
margin-bottom: 1rem;
}
.data-display {
.status-bar {
height: 60px;
background-color: #000033;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.8rem;
padding: 0 20px;
position: relative;
}
.axis-label {
color: #666;
font-size: 1rem;
.title-container {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 24px;
}
.axis-value {
color: #34495e;
font-size: 1.1rem;
.title-box {
border: 2px solid #00ffff;
padding: 5px 15px;
border-radius: 8px;
}
.main-content {
flex: 1;
display: flex;
padding: 20px;
gap: 20px;
}
.data-card {
border: 1px solid #00ffff;
border-radius: 8px;
padding: 15px;
background-color: rgba(0, 255, 255, 0.05);
}
.card-title {
color: #00ffff;
font-size: 20px;
margin-bottom: 15px;
text-align: center;
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 0;
font-size: 18px;
}
.value {
color: #00ffff;
font-weight: bold;
}
.buttons-card {
padding: 1rem;
.button-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.button-group {
.button-status {
padding: 10px;
border: 1px solid #00ffff;
border-radius: 5px;
text-align: center;
transition: all 0.3s ease;
}
.button-status.active {
background-color: #00ffff;
color: #000033;
}
.left-panel, .right-panel {
width: 200px;
}
.center-panel {
flex: 1;
}
.placeholder-div {
width: 63px;
}
.top-right-icons {
display: flex;
flex-direction: column;
gap: 0.8rem;
gap: 15px;
align-items: center;
width: 63px;
}
.button-item {
padding: 0.8rem;
border-radius: 8px;
background: rgba(255, 255, 255, 0.9);
color: #666;
font-size: 1rem;
.top-icon {
width: 24px;
height: 24px;
cursor: pointer;
transition: all 0.2s ease;
}
.button-item:hover {
background: rgba(46, 204, 113, 0.1);
}
.pressed {
background: #2ecc71;
color: white !important;
}
.pressed:hover {
background: #27ae60;
}
</style>

View File

@ -16,6 +16,8 @@
<van-cell size="large" class="custom-cell" title="车牌识别" icon="search" is-link value="识别" @click="goto('recognition')" />
<van-cell size="large" class="custom-cell" title="预警设置" icon="warning-o" is-link value="预警" @click="goto('warning')" />
<van-cell size="large" class="custom-cell" title="远程喊话" icon="bullhorn-o" is-link value="喊话" @click="navigateTo('web/voice_copy.html')" />
<van-cell size="large" class="custom-cell" title="小车控制" icon="car" is-link value="控制" @click="goto('CarControl')" />
<van-cell size="large" class="custom-cell" title="小车控制" icon="car" is-link value="控制" @click="goto('TEST')" />
<van-cell size="large" class="custom-cell" title="小车控制" icon="car" is-link value="控制" @click="goto('AudioPlay')" />
</van-cell-group>
</div>

Binary file not shown.