Files
web-Iot/src/components/devices/deviceController.js

549 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import throttle from 'lodash/throttle';
import dayjs from 'dayjs';
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, siteId) {
this.userInfo = userInfo;
this.messageApi = messageApi;
this.siteId = siteId;
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.deviceStatusChangedMsg = null;
this.onIsFinished = null;
this.onIsExecuting = null;
this.handleArrivedPoint = null;
// 全量设备实时状态(mapPush)连接
this.mapPushWs = null;
this.mapPushManualClose = false;
this.mapPushReconnectTimer = null;
this.onMapPush = 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();
}
disconnect() {
console.log('DeviceController disconnect: 断开 WebSocket');
wsManager.close();
this.connectedDeviceSn = null;
this.currentDevice = null;
this.wsDeviceInfo = {};
this.locationInfo = {};
this.isControlled = false;
this.onControlStatusChange?.(false);
}
// ============ 全量设备实时状态(mapPush)连接 ============
// 进入实时监控页时连接一次,接收后端广播的所有设备状态;退出页面时调用 closeMapPush 断开。
initMapPush(onMapPush) {
console.log('DeviceController initMapPush: 连接全量设备 WebSocket');
this.onMapPush = onMapPush || null;
this.mapPushManualClose = false;
this.connectMapPush();
}
connectMapPush() {
if (this.mapPushWs && this.mapPushWs.readyState === WebSocket.OPEN) return;
try {
this.mapPushManualClose = false;
const ws = new WebSocket(this.wsUrl);
this.mapPushWs = ws;
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'mapPush',
token: this.userInfo?.token,
siteId: this.siteId,
userName: this.userInfo?.username,
deviceId: '',
}));
};
ws.onmessage = (event) => {
let msg = null;
try { msg = JSON.parse(event.data); } catch (e) { return; }
console.log('[DeviceController] 收到全量设备状态 初始状态', msg);
if (msg?.type == 'mapPush') {
const list = Array.isArray(msg?.data) && msg.data.length
? msg.data
: [];
if (!list.length) return;
const next = {};
list.forEach((it) => {
const key = String(it?.id ?? '');
if (!key) return;
next[key] = {
id: key,
lng: String(it?.lng ?? it?.lon ?? it?.longitude),
lat: String(it?.lat ?? it?.latitude),
yaw: it?.y,
fix: it?.f !== undefined ? String(it.f) : undefined,
fixStatus: it?.f !== undefined ? String(it.f) : undefined,
altitude: it?.a !== undefined ? String(it.a) : undefined,
status: it?.s ?? it?.status,
taskId: it?.t ?? it?.taskId ?? null,
battery: it?.b !== undefined ? Number(it.b) : (it?.batteryLevel !== undefined ? Number(it.batteryLevel) : undefined),
hasAlarm: !!it?.ha || !!it?.hasAlarm,
updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
controlMode: it?.cm ?? it?.controlMode,
};
});
console.log('[DeviceController] 收到全量设备状态', next);
this.onMapPush?.(next);
}
if (msg?.type == 'device_task_change') {
if (msg.status == "FINISH") {
console.log('task finish');
this.onIsFinished?.(true);
}
if (msg.status == "EXECUTING" || msg.status == "CANCELLED" || msg.status == "FAILED") {
this.onIsExecuting?.(true);
}
}
if (msg?.type == 'device_status_changed') {
this.deviceStatusChangedMsg?.(msg);
}
if (msg?.type == 'route_post') {
console.log('route_post');
this.onRoutePost?.(msg);
}
};
ws.onerror = () => {
console.error('[DeviceController] mapPush WebSocket 错误');
};
ws.onclose = () => {
if (this.mapPushWs === ws) this.mapPushWs = null;
if (!this.mapPushManualClose) {
this.mapPushReconnectTimer = setTimeout(() => this.connectMapPush(), 5000);
}
};
} catch (err) {
console.error('[DeviceController] 连接全量设备 WebSocket 失败', err);
this.mapPushReconnectTimer = setTimeout(() => this.connectMapPush(), 5000);
}
}
closeMapPush() {
this.mapPushManualClose = true;
if (this.mapPushReconnectTimer) {
clearTimeout(this.mapPushReconnectTimer);
this.mapPushReconnectTimer = null;
}
if (this.mapPushWs) {
this.mapPushWs.onopen = null;
this.mapPushWs.onmessage = null;
this.mapPushWs.onerror = null;
this.mapPushWs.onclose = null;
try { this.mapPushWs.close(); } catch (e) { /* ignore */ }
this.mapPushWs = null;
}
}
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);
// 切换任务/设备:先清掉上一个设备的实时坐标,避免轨迹残留到新设备
this.onLocationInfoUpdate?.(null);
wsManager.close();
try {
console.log('Connecting to', this.wsUrl);
this.connectedDeviceSn = sn;
wsManager.connect(sn, this.wsUrl);
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);
// 切换设备:清掉上一个设备的实时坐标,避免轨迹残留
this.onLocationInfoUpdate?.(null);
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;
});
// 切换任务/切换设备时,忽略上一个设备遗留的实时数据
if (data?.deviceId && data.deviceId !== this.connectedDeviceSn) {
break;
}
this.wsDeviceInfo = data;
this.onDeviceInfoUpdate?.(data);
break;
}
case 'locationInfo': {
const data = {};
msg.data.forEach((item) => {
data[item.name] = item.value;
});
// 切换任务/切换设备时,忽略上一个设备遗留的实时经纬度,只显示当前连接设备的实时轨迹
if (data?.deviceId && data.deviceId !== this.connectedDeviceSn) {
break;
}
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": {
console.log('task change:', msg);
if (msg.status == "FINISH") {
console.log('task finish');
this.onIsFinished?.(true);
}
if (msg.status == "EXECUTING") {
this.onIsExecuting?.(true);
}
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':
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;
case 'device_status_changed':
this.deviceStatusChangedMsg?.(msg);
break;
case 'remoteControl':
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,
});
}
}