/**
|
* DJI无人机监控前端应用
|
*/
|
class DroneMonitorApp {
|
constructor() {
|
this.ws = null; // 控制/状态WebSocket
|
this.videoWs = null; // 视频流WebSocket
|
this.isConnected = false;
|
this.isVideoConnected = false;
|
this.reconnectAttempts = 0;
|
this.maxReconnectAttempts = 5;
|
this.reconnectInterval = 3000;
|
|
// 状态数据
|
this.droneStatus = {
|
device_sn: '',
|
is_online: false,
|
is_live: false,
|
battery_level: 0,
|
flight_mode: 'unknown',
|
coordinates: {
|
latitude: 0,
|
longitude: 0,
|
altitude: 0,
|
heading: 0,
|
pitch: 0,
|
roll: 0
|
},
|
gimbal_attitude: { // 云台姿态数据 - 参考DJI Demo
|
pitch: 0,
|
roll: 0,
|
yaw: 0
|
}
|
};
|
|
// 视频相关
|
this.videoStats = {
|
fps: 0,
|
frameCount: 0,
|
lastFrameTime: 0,
|
latency: 0
|
};
|
|
// DOM元素
|
this.elements = {};
|
|
this.init();
|
}
|
|
init() {
|
console.log('🚀 初始化DJI无人机监控系统');
|
this.initElements();
|
this.connectWebSocket();
|
this.connectVideoWebSocket(); // 连接视频流WebSocket
|
this.setupEventListeners();
|
}
|
|
initElements() {
|
// 状态指示器
|
this.elements.wsStatus = document.getElementById('wsStatus');
|
this.elements.wsStatusText = document.getElementById('wsStatusText');
|
this.elements.mqttStatus = document.getElementById('mqttStatus');
|
this.elements.mqttStatusText = document.getElementById('mqttStatusText');
|
this.elements.videoStatus = document.getElementById('videoStatus');
|
this.elements.videoStatusText = document.getElementById('videoStatusText');
|
|
// 设备信息
|
this.elements.deviceSn = document.getElementById('deviceSn');
|
this.elements.battery = document.getElementById('battery');
|
this.elements.flightMode = document.getElementById('flightMode');
|
|
// 坐标数据
|
this.elements.latitude = document.getElementById('latitude');
|
this.elements.longitude = document.getElementById('longitude');
|
this.elements.altitude = document.getElementById('altitude');
|
this.elements.heading = document.getElementById('heading');
|
this.elements.pitch = document.getElementById('pitch');
|
this.elements.roll = document.getElementById('roll');
|
|
// 云台姿态数据 - 参考DJI Demo
|
this.elements.gimbalPitch = document.getElementById('gimbalPitch');
|
this.elements.gimbalRoll = document.getElementById('gimbalRoll');
|
this.elements.gimbalYaw = document.getElementById('gimbalYaw');
|
this.elements.cameraStatus = document.getElementById('cameraStatus');
|
|
// 视频相关
|
this.elements.videoPlaceholder = document.getElementById('videoPlaceholder');
|
this.elements.videoDisplay = document.getElementById('videoDisplay');
|
this.elements.videoOverlay = document.getElementById('videoOverlay');
|
this.elements.fps = document.getElementById('fps');
|
this.elements.latency = document.getElementById('latency');
|
|
// 控制按钮
|
this.elements.startStreamBtn = document.getElementById('startStreamBtn');
|
this.elements.stopStreamBtn = document.getElementById('stopStreamBtn');
|
}
|
|
setupEventListeners() {
|
// 页面可见性变化
|
document.addEventListener('visibilitychange', () => {
|
if (document.hidden) {
|
console.log('页面隐藏,暂停某些功能');
|
} else {
|
console.log('页面显示,恢复功能');
|
if (!this.isConnected) {
|
this.connectWebSocket();
|
}
|
if (!this.isVideoConnected) {
|
this.connectVideoWebSocket();
|
}
|
}
|
});
|
|
// 窗口关闭前
|
window.addEventListener('beforeunload', () => {
|
if (this.ws) {
|
this.ws.close();
|
}
|
if (this.videoWs) {
|
this.videoWs.close();
|
}
|
});
|
}
|
|
connectWebSocket() {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
console.log('控制WebSocket已连接,跳过重复连接');
|
return;
|
}
|
|
const wsUrl = `ws://${window.location.hostname}:8765`;
|
console.log(`🔌 连接控制WebSocket: ${wsUrl}`);
|
|
this.updateWSStatus('connecting', 'WebSocket连接中...');
|
|
try {
|
this.ws = new WebSocket(wsUrl);
|
|
this.ws.onopen = () => {
|
console.log('✅ 控制WebSocket连接成功');
|
this.isConnected = true;
|
this.reconnectAttempts = 0;
|
this.updateWSStatus('online', 'WebSocket已连接');
|
this.showMessage('控制WebSocket连接成功', 'success');
|
};
|
|
this.ws.onmessage = (event) => {
|
try {
|
const message = JSON.parse(event.data);
|
console.log('📨 收到控制WebSocket消息:', message.type, '数据大小:', event.data.length);
|
this.handleMessage(message);
|
} catch (error) {
|
console.error('解析控制WebSocket消息失败:', error);
|
console.error('原始消息:', event.data.substring(0, 200) + '...');
|
}
|
};
|
|
this.ws.onclose = (event) => {
|
console.log('❌ 控制WebSocket连接关闭', event.code, event.reason);
|
this.isConnected = false;
|
this.updateWSStatus('offline', 'WebSocket已断开');
|
|
if (!event.wasClean) {
|
this.attemptReconnect();
|
}
|
};
|
|
this.ws.onerror = (error) => {
|
console.error('控制WebSocket连接错误:', error);
|
this.updateWSStatus('offline', 'WebSocket连接失败');
|
this.showMessage('控制WebSocket连接失败', 'error');
|
};
|
|
} catch (error) {
|
console.error('创建控制WebSocket连接失败:', error);
|
this.updateWSStatus('offline', 'WebSocket创建失败');
|
this.showMessage('控制WebSocket创建失败', 'error');
|
}
|
}
|
|
connectVideoWebSocket() {
|
if (this.videoWs && this.videoWs.readyState === WebSocket.OPEN) {
|
console.log('视频WebSocket已连接,跳过重复连接');
|
return;
|
}
|
|
const videoWsUrl = `ws://${window.location.hostname}:8766`;
|
console.log(`🔌 连接视频WebSocket: ${videoWsUrl}`);
|
|
try {
|
this.videoWs = new WebSocket(videoWsUrl);
|
|
this.videoWs.onopen = () => {
|
console.log('✅ 视频WebSocket连接成功');
|
this.isVideoConnected = true;
|
this.showMessage('视频WebSocket连接成功', 'success');
|
};
|
|
this.videoWs.onmessage = (event) => {
|
try {
|
const message = JSON.parse(event.data);
|
console.log('📨 收到视频WebSocket消息:', message.type);
|
this.handleVideoMessage(message);
|
} catch (error) {
|
console.error('解析视频WebSocket消息失败:', error);
|
}
|
};
|
|
this.videoWs.onclose = (event) => {
|
console.log('❌ 视频WebSocket连接关闭', event.code, event.reason);
|
this.isVideoConnected = false;
|
|
if (!event.wasClean) {
|
this.attemptVideoReconnect();
|
}
|
};
|
|
this.videoWs.onerror = (error) => {
|
console.error('视频WebSocket连接错误:', error);
|
this.showMessage('视频WebSocket连接失败', 'error');
|
};
|
|
} catch (error) {
|
console.error('创建视频WebSocket连接失败:', error);
|
this.showMessage('视频WebSocket创建失败', 'error');
|
}
|
}
|
|
attemptVideoReconnect() {
|
console.log('🔄 尝试重连视频WebSocket');
|
setTimeout(() => {
|
this.connectVideoWebSocket();
|
}, this.reconnectInterval);
|
} attemptReconnect() {
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
console.log('达到最大重连次数,停止重连');
|
this.showMessage('连接失败,请手动重新连接', 'error');
|
return;
|
}
|
|
this.reconnectAttempts++;
|
console.log(`🔄 尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
setTimeout(() => {
|
this.connectWebSocket();
|
}, this.reconnectInterval);
|
}
|
|
handleMessage(message) {
|
console.log('📨 处理控制消息:', message.type, message.data ? Object.keys(message.data) : 'no data');
|
|
switch (message.type) {
|
case 'connection_status':
|
this.handleConnectionStatus(message.data);
|
break;
|
case 'drone_status':
|
this.handleDroneStatus(message.data);
|
break;
|
case 'coordinates':
|
this.handleCoordinates(message.data);
|
break;
|
case 'gimbal_attitude': // 云台姿态消息处理 - 参考DJI Demo
|
this.handleGimbalAttitude(message.data);
|
break;
|
case 'error':
|
this.handleError(message.data);
|
break;
|
default:
|
console.log('未知控制消息类型:', message.type);
|
}
|
}
|
|
handleVideoMessage(message) {
|
console.log('📨 处理视频消息:', message.type);
|
|
switch (message.type) {
|
case 'video_frame':
|
console.log('🎥 收到视频帧,frame大小:', message.data.frame ? message.data.frame.length : 'no frame');
|
this.handleVideoFrame(message.data);
|
break;
|
case 'connection_established':
|
console.log('✅ 视频服务器连接已建立');
|
break;
|
case 'stats':
|
console.log('📊 视频服务器统计:', message.data);
|
break;
|
default:
|
console.log('未知视频消息类型:', message.type);
|
}
|
}
|
|
handleConnectionStatus(data) {
|
console.log('连接状态更新:', data);
|
if (data.status === 'connected') {
|
this.showMessage(data.message, 'success');
|
}
|
}
|
|
handleDroneStatus(data) {
|
console.log('无人机状态更新:', data);
|
this.droneStatus = { ...this.droneStatus, ...data };
|
this.updateDroneStatusDisplay();
|
}
|
|
handleCoordinates(data) {
|
if (data.coordinates) {
|
this.droneStatus.coordinates = { ...this.droneStatus.coordinates, ...data.coordinates };
|
this.updateCoordinatesDisplay();
|
}
|
}
|
|
handleGimbalAttitude(data) {
|
console.log('📹 云台姿态更新:', data);
|
if (data.gimbal_attitude) {
|
this.droneStatus.gimbal_attitude = { ...this.droneStatus.gimbal_attitude, ...data.gimbal_attitude };
|
this.updateGimbalAttitudeDisplay();
|
|
// 更新相机视锥投影 - 调用index.html中的函数
|
if (window.updateGimbalData) {
|
window.updateGimbalData(
|
this.droneStatus.gimbal_attitude.pitch,
|
this.droneStatus.gimbal_attitude.roll,
|
this.droneStatus.gimbal_attitude.yaw
|
);
|
}
|
}
|
}
|
|
handleVideoFrame(data) {
|
console.log('🎬 处理视频帧:', {
|
device_sn: data.device_sn,
|
frameSize: data.frame ? data.frame.length : 'no frame',
|
timestamp: data.timestamp,
|
width: data.width,
|
height: data.height
|
});
|
|
if (!data.frame) {
|
console.error('❌ 视频帧数据为空');
|
return;
|
}
|
|
// 更新视频帧
|
const frameData = `data:image/jpeg;base64,${data.frame}`;
|
this.elements.videoDisplay.src = frameData;
|
|
// 显示视频
|
this.elements.videoPlaceholder.style.display = 'none';
|
this.elements.videoDisplay.style.display = 'block';
|
this.elements.videoOverlay.style.display = 'block';
|
|
// 更新视频状态
|
this.updateVideoStatus('online', '视频正常');
|
|
// 计算FPS和延迟
|
this.updateVideoStats(data.timestamp);
|
|
console.log('✅ 视频帧更新完成');
|
}
|
|
handleError(data) {
|
console.error('服务器错误:', data.error);
|
this.showMessage(data.error, 'error');
|
}
|
|
updateDroneStatusDisplay() {
|
// 更新设备信息
|
this.elements.deviceSn.textContent = this.droneStatus.device_sn || '-';
|
this.elements.battery.textContent = this.droneStatus.battery_level ?
|
`${this.droneStatus.battery_level}%` : '-';
|
this.elements.flightMode.textContent = this.droneStatus.flight_mode || '-';
|
|
// 更新MQTT状态
|
if (this.droneStatus.is_online) {
|
this.updateMQTTStatus('online', 'MQTT在线');
|
} else {
|
this.updateMQTTStatus('offline', 'MQTT离线');
|
}
|
|
// 更新按钮状态
|
this.updateControlButtons();
|
}
|
|
updateCoordinatesDisplay() {
|
const coords = this.droneStatus.coordinates;
|
this.elements.latitude.textContent = coords.latitude ? coords.latitude.toFixed(6) : '-';
|
this.elements.longitude.textContent = coords.longitude ? coords.longitude.toFixed(6) : '-';
|
this.elements.altitude.textContent = coords.altitude ? `${coords.altitude.toFixed(1)}m` : '-';
|
this.elements.heading.textContent = coords.heading ? `${coords.heading.toFixed(1)}°` : '-';
|
this.elements.pitch.textContent = coords.pitch ? `${coords.pitch.toFixed(1)}°` : '-';
|
this.elements.roll.textContent = coords.roll ? `${coords.roll.toFixed(1)}°` : '-';
|
}
|
|
updateGimbalAttitudeDisplay() {
|
// 更新云台姿态显示 - 参考DJI Demo实现
|
const gimbal = this.droneStatus.gimbal_attitude;
|
if (this.elements.gimbalPitch) {
|
this.elements.gimbalPitch.textContent = gimbal.pitch ? `${gimbal.pitch.toFixed(1)}°` : '-';
|
}
|
if (this.elements.gimbalRoll) {
|
this.elements.gimbalRoll.textContent = gimbal.roll ? `${gimbal.roll.toFixed(1)}°` : '-';
|
}
|
if (this.elements.gimbalYaw) {
|
this.elements.gimbalYaw.textContent = gimbal.yaw ? `${gimbal.yaw.toFixed(1)}°` : '-';
|
}
|
}
|
|
updateVideoStats(frameTimestamp) {
|
const now = Date.now();
|
|
// 计算延迟
|
this.videoStats.latency = now - (frameTimestamp * 1000);
|
|
// 计算FPS
|
if (this.videoStats.lastFrameTime > 0) {
|
const timeDiff = (now - this.videoStats.lastFrameTime) / 1000;
|
this.videoStats.fps = 1 / timeDiff;
|
}
|
this.videoStats.lastFrameTime = now;
|
this.videoStats.frameCount++;
|
|
// 更新显示
|
this.elements.fps.textContent = this.videoStats.fps.toFixed(1);
|
this.elements.latency.textContent = this.videoStats.latency.toFixed(0);
|
}
|
|
updateControlButtons() {
|
const isOnline = this.droneStatus.is_online;
|
const isLive = this.droneStatus.is_live;
|
|
this.elements.startStreamBtn.disabled = !isOnline || isLive;
|
this.elements.stopStreamBtn.disabled = !isOnline || !isLive;
|
}
|
|
updateWSStatus(status, text) {
|
this.elements.wsStatus.className = `status-indicator status-${status}`;
|
this.elements.wsStatusText.textContent = text;
|
}
|
|
updateMQTTStatus(status, text) {
|
this.elements.mqttStatus.className = `status-indicator status-${status}`;
|
this.elements.mqttStatusText.textContent = text;
|
}
|
|
updateVideoStatus(status, text) {
|
this.elements.videoStatus.className = `status-indicator status-${status}`;
|
this.elements.videoStatusText.textContent = text;
|
}
|
|
showMessage(text, type = 'info') {
|
const messageDiv = document.createElement('div');
|
messageDiv.className = `message ${type}`;
|
messageDiv.textContent = text;
|
|
document.getElementById('messageContainer').appendChild(messageDiv);
|
|
// 显示消息
|
setTimeout(() => messageDiv.classList.add('show'), 100);
|
|
// 自动隐藏
|
setTimeout(() => {
|
messageDiv.classList.remove('show');
|
setTimeout(() => messageDiv.remove(), 300);
|
}, 3000);
|
}
|
|
sendMessage(type, data = {}) {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
const message = {
|
type: type,
|
data: data,
|
timestamp: Date.now() / 1000
|
};
|
this.ws.send(JSON.stringify(message));
|
return true;
|
} else {
|
console.error('WebSocket未连接,无法发送消息');
|
this.showMessage('WebSocket未连接', 'error');
|
return false;
|
}
|
}
|
}
|
|
// 全局函数
|
let app;
|
|
window.onload = function() {
|
app = new DroneMonitorApp();
|
};
|
|
function reconnectWebSocket() {
|
console.log('🔄 手动重新连接WebSocket');
|
app.reconnectAttempts = 0;
|
app.connectWebSocket();
|
app.connectVideoWebSocket();
|
}
|
|
function startStream() {
|
console.log('▶️ 开始直播');
|
app.sendMessage('start_stream');
|
app.showMessage('正在启动直播...', 'info');
|
}
|
|
function stopStream() {
|
console.log('⏹️ 停止直播');
|
app.sendMessage('stop_stream');
|
app.showMessage('正在停止直播...', 'info');
|
|
// 隐藏视频
|
app.elements.videoDisplay.style.display = 'none';
|
app.elements.videoOverlay.style.display = 'none';
|
app.elements.videoPlaceholder.style.display = 'block';
|
app.updateVideoStatus('offline', '视频离线');
|
}
|
|
function toggleFullscreen() {
|
const videoContainer = document.querySelector('.video-container');
|
|
if (!document.fullscreenElement) {
|
videoContainer.requestFullscreen().catch(err => {
|
console.error('进入全屏失败:', err);
|
app.showMessage('全屏功能不可用', 'error');
|
});
|
} else {
|
document.exitFullscreen();
|
}
|
}
|