import throttle from 'lodash/throttle'; import { wsManager } from '../WebSocketManager.ts'; import { getJurisdiction, releaseControl } from '../../api/device.ts'; import envConfig from '../../../env'; import i18n from '../../i18n'; 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.locationInfo = {}; this.controlTimeout = null; this.keepAliveInterval = null; this.onDeviceInfoUpdate = null; this.onLocationInfoUpdate = null; this.onControlStatusChange = null; this.onWsStatusChange = null; this.onHasControlRightChange = null; this.onRequestControlStatusChange = null; this.onPermissionRequest = null; this.onRoutePost = 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.handleBeforeUnload = this.handleBeforeUnload.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); document.addEventListener('beforeunload', this.handleBeforeUnload); } levae() { wsManager.close(); } handleVisibilityChange() { if (document.visibilityState === 'hidden') { console.log('Page hidden, release control'); //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('Page visible, reconnect WebSocket'); setTimeout(() => { this.connectDevice(this.currentDevice); }, 500); } } handleBeforeUnload() { console.log('Browser closing/refreshing, release control'); this.release(); } startKeepAlive() { this.keepAliveInterval = setInterval(() => { if ( this.connectedDeviceSn && this.wsStatus !== 'connected' && this.currentDevice?.serialNumber ) { console.log('Keep alive: reconnect WebSocket'); this.connectDevice(this.currentDevice); } }, 8000); } async release() { if (!this.connectedDeviceSn) return; const data = { platform: 'web', type: 1, deviceId: this.connectedDeviceSn, token: this.userInfo.token, }; const url = `${envConfig.baseURL}/forward/device/releaseControl`; try { await releaseControl(data); const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); navigator.sendBeacon(url, blob); console.log('releaseControl sent'); } catch (e) { console.error('Failed to send releaseControl:', e); } } destroy() { this.clearControlTimeout(); if (this.keepAliveInterval) { clearInterval(this.keepAliveInterval); this.keepAliveInterval = null; } document.removeEventListener('visibilitychange', this.handleVisibilityChange); document.removeEventListener('beforeunload', this.handleBeforeUnload); this.release(); this.levae(); this.unSubMessage?.(); this.unSubStatus?.(); 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.locationInfo = {}; this.isControlled = false; this.onControlStatusChange?.(false); wsManager.close(); try { console.log('Connecting to', this.wsUrl); wsManager.connect(sn, this.wsUrl); this.connectedDeviceSn = sn; await this.getControl(sn); } catch (err) { console.error(err); this.messageApi.error(i18n.t('devices.failedFixed22')); } } async switchDevice(device) { if (!device) return; if (!device.serialNumber) return; const newSn = device.serialNumber; if (this.connectedDeviceSn === newSn && this.wsStatus === 'connected') { console.log('switchDevice: same device, skip'); return; } console.log('Connecting to switchDevice: switching to', newSn); this.currentDevice = device; this.connectedDeviceSn = newSn; this.wsDeviceInfo = {}; this.locationInfo = {}; this.isControlled = false; this.onControlStatusChange?.(false); wsManager.send({ type: 'AUTH', token: this.userInfo.token, userName: this.userInfo.username, deviceId: newSn, }); await this.getControl(newSn); } async getControl(serialnum) { try { const res = await getJurisdiction({ platform: 'web', deviceId: serialnum, token: this.userInfo.token, }); 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; this.isRequestControl = true; this.onRequestControlStatusChange?.(true); this.messageApi.loading({ key: 'control', content: i18n.t('devices.control2'), 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(i18n.t('devices.fixed2ControlFixed2')); 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('WS message:', 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 'locationInfo': { const data = {}; msg.data.forEach((item) => { data[item.name] = item.value; }); this.locationInfo = data; this.onLocationInfoUpdate?.(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 || i18n.t('devices.controlFailed') }); return; } this.isControlled = true; this.onControlStatusChange?.(true); this.messageApi.success({ key: 'control', content: i18n.t('devices.fixed2ControlFixed2SuccessFixed2') }); break; } case "device_task_change": { if (msg.deviceId != this.connectedDeviceSn) return; this.onIsFinished?.(true); console.log('task change:', msg); break; } case "device_task_arrive_point": { if (msg.deviceId != this.connectedDeviceSn) return; this.handleArrivedPoint?.(msg.entity); console.log('arrive point:', msg); break; } case 'switchPermission': { const platform = (msg.platform || '').toLowerCase(); if (!platform || platform === 'null') return; this.requestDeviceId = msg.deviceId; this.onPermissionRequest?.({ title: i18n.t('devices.s2'), content: i18n.t('devices.platformRequestControl', { platform: 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: i18n.t('constants.locationUnknown'), 1: i18n.t('devices.obstaclePerson'), 2: i18n.t('devices.obstacleVehicle'), 3: i18n.t('devices.obstacleFence'), 4: i18n.t('devices.obstaclePothole'), 5: i18n.t('devices.obstacleGrass') }; this.messageApi.warning({ key: 'obstacle-warning', content: i18n.t('devices.obstacleDetectedReport', { obstacle: types[type] ?? i18n.t('devices.unknownObject'), distance }), duration: 3, }); break; case 'route_post': console.log('route post:', msg); if (msg.deviceId) { this.onRoutePost?.(msg); } 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, }); } }