import throttle from 'lodash/throttle'; import { wsManager } from '../WebSocketManager.ts'; import { getControlRight, getJurisdiction, } from '../../api/device.ts'; import envConfig from '../../../env'; export default class DeviceController { constructor(userInfo, messageApi) { this.userInfo = userInfo; this.messageApi = messageApi; this.wsUrl = envConfig.WS_URL || 'wss://ri.satabot.com/ws/'; this.connectedDeviceSn = null; this.currentDevice = null; // 保存当前设备 this.requestDeviceId = ''; this.isControlled = false; this.isRequestControl = false; this.hasControlRight = false; this.wsStatus = 'disconnected'; this.wsDeviceInfo = {}; this.controlTimeout = null; this.keepAliveInterval = null; // 后台保活定时器 this.onDeviceInfoUpdate = null; this.onControlStatusChange = null; this.onWsStatusChange = null; this.onHasControlRightChange = null; this.onRequestControlStatusChange = null; this.onPermissionRequest = null; this.onIsFinished = null; this.handleArrivedPoint = null; this.handleWsMessage = this.handleWsMessage.bind(this); this.handleWsStatus = this.handleWsStatus.bind(this); this.handleVisibilityChange = this.handleVisibilityChange.bind(this); this.sendGamepad = throttle( this.sendGamepad.bind(this), 50, { leading: true, trailing: true } ); } init() { this.unSubMessage = wsManager.onMessage(this.handleWsMessage); this.unSubStatus = wsManager.onStatusChange(this.handleWsStatus); // ========== 【集成】页面可见性监听 ========== document.addEventListener('visibilitychange', this.handleVisibilityChange); // ========== 【集成】后台保活定时 ========== //this.startKeepAlive(); } // ========== 【集成】页面切前台重连 ========== handleVisibilityChange() { // 切后台:关闭 WS if (document.visibilityState === 'hidden') { console.log('🔌 页面切后台,关闭 WebSocket'); this.release(); wsManager.close(); this.connectedDeviceSn = null; this.isControlled = false; this.onControlStatusChange?.(false); } if (document.visibilityState === 'visible' && this.currentDevice) { if (!this.currentDevice?.serialNumber) return; console.log('✅ 页面切回前台,自动重连 WebSocket'); setTimeout(() => { this.connectDevice(this.currentDevice); }, 500); } } // ========== 【集成】后台保活 ========== startKeepAlive() { this.keepAliveInterval = setInterval(() => { if ( this.connectedDeviceSn && this.wsStatus !== 'connected' && this.currentDevice?.serialNumber ) { console.log('🔄 后台保活:尝试重连 WebSocket'); this.connectDevice(this.currentDevice); } }, 8000); } release() { if (this.isControlled && this.connectedDeviceSn) { const data = { platform: "web", type: 1 }; const url = `${envConfig.baseURL}/forward/device/releaseControl`; try { const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); navigator.sendBeacon(url, blob); } catch (e) { console.error('Failed to send releaseControl beacon:', e); } } } destroy() { this.clearControlTimeout(); // 清理保活 if (this.keepAliveInterval) { clearInterval(this.keepAliveInterval); this.keepAliveInterval = null; } // 移除监听 document.removeEventListener('visibilitychange', this.handleVisibilityChange); this.unSubMessage?.(); this.unSubStatus?.(); this.release(); //wsManager.close(); this.connectedDeviceSn = null; } handleWsStatus(status) { this.wsStatus = status; this.onWsStatusChange?.(status); if (status === 'disconnected') { this.isControlled = false; this.onControlStatusChange?.(false); } } async connectDevice(device) { if (!device) return; // 保存当前设备,用于保活/重连 this.currentDevice = device; if (!device.serialNumber) return; const sn = device.serialNumber; this.connectedDeviceSn = null; this.wsDeviceInfo = {}; this.isControlled = false; this.onControlStatusChange?.(false); //wsManager.close(); try { const res = await getControlRight({ userName: this.userInfo.username, sourceType: 1, token: this.userInfo.token, }); this.hasControlRight = res?.code === 200 && res.data == true; this.onHasControlRightChange?.(this.hasControlRight); wsManager.connect(sn, this.wsUrl); this.connectedDeviceSn = sn; if (!this.hasControlRight) { this.messageApi.warning('其他端正在控制'); return; } await this.getControl(sn); } catch (err) { console.error(err); this.messageApi.error('权限校验失败'); } } async getControl(serialnum) { try { const res = await getJurisdiction({ platform: 'web', deviceId: serialnum, }); this.isControlled = res?.code === 200 && res?.data?.remoteControl === true; this.onControlStatusChange?.(this.isControlled); } catch (err) { console.error(err); this.isControlled = false; this.onControlStatusChange?.(false); } } requestControl() { if (this.isRequestControl) return; if (!this.hasControlRight) { this.messageApi.warning('无控制权限'); return; } this.isRequestControl = true; this.onRequestControlStatusChange?.(true); this.messageApi.loading({ key: 'control', content: '请求控制中...', duration: 0, }); this.sendPermissionRequest(true, 'switchPermission'); this.clearControlTimeout(); this.controlTimeout = setTimeout(() => { if (!this.isRequestControl) return; this.isRequestControl = false; this.onRequestControlStatusChange?.(false); this.messageApi.destroy('control'); this.messageApi.error('控制请求超时'); this.clearControlTimeout(); }, 20000); } clearControlTimeout() { if (this.controlTimeout) { clearTimeout(this.controlTimeout); this.controlTimeout = null; } } sendPermissionRequest(data, type) { wsManager.send({ type, switchResult: data, token: this.userInfo.token, deviceId: type === 'switchPermission' ? this.connectedDeviceSn : this.requestDeviceId, userName: this.userInfo.username, }); } handleWsMessage(msg) { console.log('📨 收到消息:', msg); switch (msg.type) { case 'deviceInfo': { const data = {}; msg.data.forEach((item) => { data[item.name] = item.value; }); this.wsDeviceInfo = data; this.onDeviceInfoUpdate?.(data); break; } case 'AUTH': { if (msg.isAuth) { this.getControl(this.connectedDeviceSn); } break; } case 'switchResult': { this.clearControlTimeout(); this.isRequestControl = false; this.onRequestControlStatusChange?.(false); if (msg.success === false || msg?.respond?.switchResult === false) { this.isControlled = false; this.onControlStatusChange?.(false); this.messageApi.error({ key: 'control', content: msg.message || '控制失败' }); return; } this.isControlled = true; this.onControlStatusChange?.(true); this.messageApi.success({ key: 'control', content: '控制成功' }); break; } case "device_task_change": { if (msg.deviceId != this.connectedDeviceSn) return; this.onIsFinished?.(true); console.log('🔌 控制结果任务变更,', msg); } //完成点 case "device_task_arrive_point": { if (msg.deviceId != this.connectedDeviceSn) return; this.handleArrivedPoint?.(msg.entity); console.log('🔌 到达完成点,', msg); } case 'switchPermission': { const platform = (msg.platform || '').toLowerCase(); if (!platform || platform === 'null') return; this.requestDeviceId = msg.deviceId; this.onPermissionRequest?.({ title: '权限请求', content: `${msg.platform} 请求控制设备`, }); break; } case 'detectionReport': if (msg.data?.deviceId !== this.connectedDeviceSn) return; const { isSecure, obstacle } = msg.data; if (isSecure) return; const type = obstacle?.type; const distance = (obstacle?.distance || 0).toFixed(2); const types = { 0: '未知', 1: '人', 2: '车辆', 3: '围栏', 4: '坑洞', 5: '草丛' }; this.messageApi.warning({ key: 'obstacle-warning', content: `检测到 ${types[type] ?? '未知物'},距离 ${distance}m`, duration: 3, }); break; default: break; } } sendGamepad(state) { if (!this.isControlled || !this.connectedDeviceSn) return; wsManager.send({ type: 'remoteControl', remoteData: state, token: this.userInfo.token, userName: this.userInfo.username, }); } }