2.19
This commit is contained in:
parent
55833cbdc9
commit
960a08f25d
40
public/audio-processor.js
Normal file
40
public/audio-processor.js
Normal 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);
|
@ -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');
|
||||
}
|
||||
|
||||
// 手动获取手柄数据的方法
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
if (this.options.debug) {
|
||||
console.log("Gamepad disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
// 导出模块
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = GamepadController;
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
define([], () => GamepadController);
|
||||
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 {
|
||||
window.GamepadController = GamepadController;
|
||||
this.directionAxis9 = "未定义";
|
||||
}
|
||||
}
|
||||
|
||||
pressKey(buttons) {
|
||||
this.buttons.forEach(button => {
|
||||
const buttonData = buttons[button.index];
|
||||
button.pressed = buttonData ? buttonData.value === 1 : false;
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
clearInterval(this.interval);
|
||||
window.removeEventListener("gamepadconnected", this.onGamepadConnected);
|
||||
window.removeEventListener("gamepaddisconnected", this.onGamepadDisconnected);
|
||||
if (this.options.debug) {
|
||||
console.log("GamepadController destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类以便外部使用
|
||||
export default GamepadController;
|
@ -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',
|
||||
|
@ -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,12 +41,24 @@ 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 = 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 连接已打开');
|
||||
};
|
||||
@ -43,40 +80,70 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
const constraints = {
|
||||
audio: {
|
||||
sampleRate: parseInt(this.audioConfig.sampleRate),
|
||||
channelCount: parseInt(this.audioConfig.channels),
|
||||
sampleSize: parseInt(this.audioConfig.bitsPerSample)
|
||||
}
|
||||
});
|
||||
};
|
||||
this.mediaRecorder.start(100); // 每100ms捕获一次音频数据
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('无法访问麦克风:', err);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.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>
|
@ -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">
|
||||
<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>
|
@ -1,38 +1,56 @@
|
||||
<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 class="data-display">
|
||||
<span class="axis-label">Y 轴:</span>
|
||||
<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="data-card buttons-card">
|
||||
<h3>按键状态</h3>
|
||||
<div class="button-group">
|
||||
<div
|
||||
v-for="(button, index) in buttons"
|
||||
<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-row">
|
||||
<span>Y轴:</span>
|
||||
<span class="value">{{ directionAxis0_1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</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-item"
|
||||
:class="{ 'pressed': button.pressed }"
|
||||
>
|
||||
class="button-status"
|
||||
:class="{ 'active': button.pressed }">
|
||||
{{ button.name }}
|
||||
</div>
|
||||
</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 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>
|
||||
@ -47,6 +65,7 @@ export default {
|
||||
return {
|
||||
interval: null,
|
||||
gamepad: null,
|
||||
gamepadIndex: null,
|
||||
buttons: [
|
||||
{ name: "Left2", pressed: false },
|
||||
{ name: "Back", pressed: false },
|
||||
@ -67,7 +86,8 @@ export default {
|
||||
methods: {
|
||||
onGamepadConnected(e) {
|
||||
console.log("Gamepad connected:", e.gamepad);
|
||||
this.gamepad = navigator.getGamepads()[e.gamepad.index];
|
||||
this.gamepadIndex = e.gamepad.index;
|
||||
this.gamepad = navigator.getGamepads()[this.gamepadIndex];
|
||||
this.startGamepad();
|
||||
},
|
||||
onGamepadDisconnected() {
|
||||
@ -76,8 +96,16 @@ export default {
|
||||
},
|
||||
startGamepad() {
|
||||
this.interval = setInterval(() => {
|
||||
const gamepad = navigator.getGamepads()[0];
|
||||
const gamepads = navigator.getGamepads();
|
||||
const gamepad = gamepads[this.gamepadIndex];
|
||||
|
||||
if (gamepad) {
|
||||
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);
|
||||
@ -87,16 +115,22 @@ export default {
|
||||
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 {
|
||||
|
||||
// 计算方向角度(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) {
|
||||
@ -126,90 +160,117 @@ 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>
|
@ -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>
|
||||
|
BIN
遥控四轮车通信协议v0.3.0.250218_alpha.docx
Normal file
BIN
遥控四轮车通信协议v0.3.0.250218_alpha.docx
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user