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 { class GamepadController {
constructor(config = {}) { constructor(options = {}) {
this.config = { this.options = {
buttonMapping: {}, ...{
axisMapping: {}, debug: false,
...config 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.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, { window.addEventListener("gamepadconnected", this.onGamepadConnected.bind(this));
0: 'A', window.addEventListener("gamepaddisconnected", this.onGamepadDisconnected.bind(this));
1: 'B',
2: 'X',
3: 'Y',
// 其他默认映射
});
// 初始化默认摇杆映射 if (this.options.debug) {
Object.assign(this.config.axisMapping, { console.log("GamepadController initialized with options:", this.options);
0: 'leftX', }
1: 'leftY',
2: 'rightX',
3: 'rightY',
// 其他默认映射
});
} }
connect() { onGamepadConnected(e) {
window.addEventListener('gamepadconnected', (e) => { console.log("Gamepad connected:", e.gamepad);
this.gamepad = navigator.getGamepads()[e.gamepad.index]; this.gamepadIndex = e.gamepad.index;
this.triggerEvent('connected'); this.gamepad = navigator.getGamepads()[this.gamepadIndex];
}); this.startGamepad();
} }
disconnect() { onGamepadDisconnected() {
window.removeEventListener('gamepadconnected', this.handleConnect); clearInterval(this.interval);
this.gamepad = null; this.gamepad = null;
this.triggerEvent('disconnected'); if (this.options.debug) {
console.log("Gamepad disconnected");
}
} }
// 手动获取手柄数据的方法 startGamepad() {
pollGamepad() { this.interval = setInterval(() => {
if (!this.gamepad) return; const gamepads = navigator.getGamepads();
const gamepad = gamepads[this.gamepadIndex];
// 处理摇杆数据
const axesData = this.gamepad.axes; if (gamepad) {
const axisEvents = {}; // 注释掉调试打印
Object.entries(this.config.axisMapping).forEach(([index, name]) => { // if (this.options.debug) {
axisEvents[name] = axesData[index]; // console.log('Axes data:', {
}); // axis0: gamepad.axes[0],
this.triggerEvent('axisChange', axisEvents); // axis1: gamepad.axes[1],
// gamepadIndex: this.gamepadIndex
// 处理按键事件 // });
const buttons = this.gamepad.buttons; // }
buttons.forEach((btn, index) => {
if (btn.value === 1 && this.config.buttonMapping[index]) { this.updateDirection(gamepad.axes);
const keyName = this.config.buttonMapping[index]; this.updateDirectionAxis9(gamepad.axes);
this.triggerEvent('buttonPress', { keyName, index }); 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) { destroy() {
if (!this.listeners[type]) this.listeners[type] = []; clearInterval(this.interval);
this.listeners[type].push(callback); window.removeEventListener("gamepadconnected", this.onGamepadConnected);
} window.removeEventListener("gamepaddisconnected", this.onGamepadDisconnected);
if (this.options.debug) {
triggerEvent(type, data = {}) { console.log("GamepadController destroyed");
if (this.listeners[type]) {
this.listeners[type].forEach(callback => callback(data));
} }
} }
} }
// 导出模块 // 导出类以便外部使用
if (typeof module !== 'undefined' && module.exports) { export default GamepadController;
module.exports = GamepadController;
} else if (typeof define === 'function' && define.amd) {
define([], () => GamepadController);
} else {
window.GamepadController = GamepadController;
}

View File

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

View File

@ -5,6 +5,31 @@
{{ isRecording ? '停止传输音频' : '开始传输音频' }} {{ isRecording ? '停止传输音频' : '开始传输音频' }}
</button> </button>
<p>{{ status }}</p> <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> </div>
</template> </template>
@ -16,67 +41,109 @@ export default {
mediaRecorder: null, // MediaRecorder mediaRecorder: null, // MediaRecorder
audioChunks: [], // audioChunks: [], //
status: '点击按钮开始传输音频', status: '点击按钮开始传输音频',
isRecording: false // isRecording: false, //
audioConfig: {
sampleRate: 16000,
bitsPerSample: 16,
channels: 1
},
stream: null,
audioContext: null,
worklet: null,
// lastSendTime: 0,
// sendInterval: 200, // 500ms
}; };
}, },
mounted() { mounted() {
// WebSocket // WebSocket
this.ws = new WebSocket('ws://192.168.1.60:81'); // ESP32 IP this.ws = new WebSocket('ws://192.168.4.103/ws'); // ESP32 IP
this.ws.onopen = () => { // this.ws = new WebSocket('ws://192.168.1.60:81'); // ESP32 IP
console.log('WebSocket连接已打开');
this.ws.onopen = () => {
console.log('WebSocket 连接已打开');
}; };
this.ws.onmessage = (event) => { this.ws.onmessage = (event) => {
console.log('接收到来自ESP32的消息:', event.data); console.log(' 接收到来自ESP32的消息:', event.data);
}; };
this.ws.onclose = () => { this.ws.onclose = () => {
console.log('WebSocket连接已关闭'); console.log('WebSocket 连接已关闭');
}; };
}, },
methods: { methods: {
toggleRecording() { toggleRecording() {
if (this.isRecording) { if (this.isRecording) {
// //
this.stopRecording(); this.stopRecording();
} else { } else {
// //
this.startRecording(); this.startRecording();
} }
}, },
startRecording() { async startRecording() {
this.status = '开始录音并传输音频...'; this.status = '开始录音并传输音频...';
this.audioChunks = [];
this.isRecording = true; this.isRecording = true;
navigator.mediaDevices.getUserMedia({ audio: true }) const constraints = {
.then((stream) => { audio: {
this.mediaRecorder = new MediaRecorder(stream); sampleRate: parseInt(this.audioConfig.sampleRate),
this.mediaRecorder.ondataavailable = (event) => { channelCount: parseInt(this.audioConfig.channels),
this.audioChunks.push(event.data); sampleSize: parseInt(this.audioConfig.bitsPerSample)
// }
const audioBlob = new Blob([event.data], { type: 'audio/wav' }); };
audioBlob.arrayBuffer().then((arrayBuffer) => {
// try {
console.log('实时音频数据:', new Uint8Array(arrayBuffer)); const stream = await navigator.mediaDevices.getUserMedia(constraints);
// WebSocket
if (this.ws.readyState === WebSocket.OPEN) { // AudioContext
this.ws.send(arrayBuffer); const audioContext = new AudioContext({
} sampleRate: parseInt(this.audioConfig.sampleRate)
});
};
this.mediaRecorder.start(100); // 100ms
})
.catch((err) => {
console.error('无法访问麦克风:', err);
}); });
// 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() { stopRecording() {
this.status = '停止录音'; this.status = '停止录音';
this.isRecording = false; this.isRecording = false;
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') { if (this.stream) {
this.mediaRecorder.stop(); // 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; font-size: 16px;
cursor: pointer; 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-container">
<div class="title-box"> <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> </div>
<div class="top-right-icons"> <div class="top-right-icons">
<img src="../assets/img/connect.png" alt="link" class="top-icon"> <img :src="connectionImage" alt="link" class="top-icon">
<img src="../assets/img/bat_empty.png" alt="mp3" class="top-icon"> <img :src="batteryImage" alt="battery" class="top-icon">
</div> </div>
</div> </div>
@ -33,7 +34,7 @@
</div> </div>
<div class="switch-item"> <div class="switch-item">
<span>避障</span> <span>避障</span>
<el-switch v-model="obstacleStatus" /> <el-switch v-model="obstacleStatus.top" />
</div> </div>
</div> </div>
@ -41,7 +42,11 @@
<div class="center-display"> <div class="center-display">
<div class="status-text">云台状态</div> <div class="status-text">云台状态</div>
<div class="car-display"> <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>
</div> </div>
@ -50,15 +55,22 @@
<div class="status-icons"> <div class="status-icons">
<div class="icons-row"> <div class="icons-row">
<div class="icon-item"> <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>
<div class="icon-item"> <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>
</div> </div>
<div class="control-button"> <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> </div>
</div> </div>
@ -66,6 +78,9 @@
</template> </template>
<script> <script>
import { useRouter } from 'vue-router';
import GamepadController from '../assets/js/GamepadController';
export default { export default {
name: 'CarControl', name: 'CarControl',
data() { data() {
@ -73,7 +88,241 @@ export default {
screenStatus: false, screenStatus: false,
warningLightStatus: false, warningLightStatus: false,
followStatus: 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 { .title-box {
border: 2px solid #00ffff; border: 2px solid #00ffff;
padding: 5px 15px; padding: 2px;
border-radius: 8px; border-radius: 8px;
} }
.main-content { .main-content {
flex: 1; flex: 1;
display: flex; display: flex;
padding: 20px; padding: 10px;
gap: 20px; gap: 20px;
} }
@ -151,7 +400,7 @@ export default {
flex: 1; flex: 1;
border: 1px solid #00ffff; border: 1px solid #00ffff;
border-radius: 8px; border-radius: 8px;
padding: 20px; padding: 5px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@ -172,6 +421,27 @@ export default {
width: 100%; 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 { .car-image {
width: 160px; width: 160px;
height: auto; height: auto;
@ -237,4 +507,36 @@ export default {
height: 24px; height: 24px;
cursor: pointer; 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> </style>

View File

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

Binary file not shown.