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

549 lines
14 KiB
JavaScript
Raw Normal View History

2026-05-20 15:12:51 +08:00
import throttle from 'lodash/throttle';
import dayjs from 'dayjs';
import { wsManager } from '../WebSocketManager.ts';
2026-05-20 15:12:51 +08:00
import {
getJurisdiction,
releaseControl
2026-05-20 15:12:51 +08:00
} from '../../api/device.ts';
import envConfig from '../../../env';
2026-08-14 09:02:52 +08:00
import i18n from '../../i18n';
export default class DeviceController {
constructor(userInfo, messageApi, siteId) {
this.userInfo = userInfo;
this.messageApi = messageApi;
this.siteId = siteId;
2026-05-20 15:12:51 +08:00
this.wsUrl =
envConfig.WS_URL || 'wss://ri.satabot.com/ws/';
this.connectedDeviceSn = null;
this.currentDevice = null;
2026-05-20 15:12:51 +08:00
this.requestDeviceId = '';
2026-05-20 15:12:51 +08:00
this.isControlled = false;
this.isRequestControl = false;
this.hasControlRight = false;
2026-05-20 15:12:51 +08:00
this.wsStatus = 'disconnected';
2026-05-20 15:12:51 +08:00
this.wsDeviceInfo = {};
this.locationInfo = {};
2026-05-20 15:12:51 +08:00
this.controlTimeout = null;
this.keepAliveInterval = null;
this.onDeviceInfoUpdate = null;
this.onLocationInfoUpdate = null;
this.onControlStatusChange = null;
this.onWsStatusChange = null;
2026-05-20 15:12:51 +08:00
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);
2026-05-20 15:12:51 +08:00
this.handleWsStatus = this.handleWsStatus.bind(this);
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
this.handleBeforeUnload = this.handleBeforeUnload.bind(this);
2026-05-20 15:12:51 +08:00
this.sendGamepad = throttle(
this.sendGamepad.bind(this),
50,
{ leading: true, trailing: true }
2026-05-20 15:12:51 +08:00
);
}
init() {
this.unSubMessage = wsManager.onMessage(this.handleWsMessage);
this.unSubStatus = wsManager.onStatusChange(this.handleWsStatus);
2026-05-20 15:12:51 +08:00
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();
2026-06-04 15:39:46 +08:00
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);
2026-06-04 15:39:46 +08:00
}
}
destroy() {
2026-05-20 15:12:51 +08:00
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();
2026-05-20 15:12:51 +08:00
this.unSubMessage?.();
this.unSubStatus?.();
2026-06-04 15:39:46 +08:00
this.connectedDeviceSn = null;
}
2026-05-20 15:12:51 +08:00
handleWsStatus(status) {
this.wsStatus = status;
this.onWsStatusChange?.(status);
if (status === 'disconnected') {
this.isControlled = false;
2026-05-20 15:12:51 +08:00
this.onControlStatusChange?.(false);
}
}
async connectDevice(device) {
if (!device) return;
this.currentDevice = device;
if (!device.serialNumber) return;
2026-05-20 15:12:51 +08:00
const sn = device.serialNumber;
this.connectedDeviceSn = null;
this.wsDeviceInfo = {};
this.locationInfo = {};
2026-05-20 15:12:51 +08:00
this.isControlled = false;
this.onControlStatusChange?.(false);
// 切换任务/设备:先清掉上一个设备的实时坐标,避免轨迹残留到新设备
this.onLocationInfoUpdate?.(null);
2026-06-12 11:03:38 +08:00
wsManager.close();
2026-05-20 15:12:51 +08:00
try {
console.log('Connecting to', this.wsUrl);
this.connectedDeviceSn = sn;
wsManager.connect(sn, this.wsUrl);
2026-05-20 15:12:51 +08:00
await this.getControl(sn);
} catch (err) {
2026-05-20 15:12:51 +08:00
console.error(err);
2026-08-14 09:02:52 +08:00
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);
}
2026-05-20 15:12:51 +08:00
async getControl(serialnum) {
try {
const res = await getJurisdiction({
platform: 'web',
deviceId: serialnum,
token: this.userInfo.token,
2026-05-20 15:12:51 +08:00
});
this.isControlled = res?.code === 200 && res?.data?.remoteControl === true;
this.onControlStatusChange?.(this.isControlled);
2026-05-20 15:12:51 +08:00
} catch (err) {
console.error(err);
this.isControlled = false;
this.onControlStatusChange?.(false);
}
}
requestControl() {
if (this.isRequestControl) return;
2026-05-20 15:12:51 +08:00
this.isRequestControl = true;
2026-05-20 15:12:51 +08:00
this.onRequestControlStatusChange?.(true);
this.messageApi.loading({
key: 'control',
2026-08-14 09:02:52 +08:00
content: i18n.t('devices.control2'),
duration: 0,
});
2026-05-20 15:12:51 +08:00
this.sendPermissionRequest(true, 'switchPermission');
2026-05-20 15:12:51 +08:00
this.clearControlTimeout();
this.controlTimeout = setTimeout(() => {
if (!this.isRequestControl) return;
this.isRequestControl = false;
this.onRequestControlStatusChange?.(false);
this.messageApi.destroy('control');
2026-08-14 09:02:52 +08:00
this.messageApi.error(i18n.t('devices.fixed2ControlFixed2'));
this.clearControlTimeout();
}, 20000);
2026-05-20 15:12:51 +08:00
}
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,
2026-05-20 15:12:51 +08:00
userName: this.userInfo.username,
});
}
handleWsMessage(msg) {
console.log('WS message:', msg);
switch (msg.type) {
2026-05-20 15:12:51 +08:00
case 'deviceInfo': {
const data = {};
2026-05-20 15:12:51 +08:00
msg.data.forEach((item) => {
data[item.name] = item.value;
});
// 切换任务/切换设备时,忽略上一个设备遗留的实时数据
if (data?.deviceId && data.deviceId !== this.connectedDeviceSn) {
break;
}
this.wsDeviceInfo = data;
2026-05-20 15:12:51 +08:00
this.onDeviceInfoUpdate?.(data);
break;
2026-05-20 15:12:51 +08:00
}
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;
}
2026-05-20 15:12:51 +08:00
case 'AUTH': {
if (msg.isAuth) {
this.getControl(this.connectedDeviceSn);
}
break;
2026-05-20 15:12:51 +08:00
}
case 'switchResult': {
2026-05-20 15:12:51 +08:00
this.clearControlTimeout();
this.isRequestControl = false;
this.onRequestControlStatusChange?.(false);
if (msg?.success === false || msg?.respond?.switchResult === false) {
2026-05-20 15:12:51 +08:00
this.isControlled = false;
this.onControlStatusChange?.(false);
2026-08-14 09:02:52 +08:00
this.messageApi.error({ key: 'control', content: msg.message || i18n.t('devices.controlFailed') });
2026-05-20 15:12:51 +08:00
return;
}
this.isControlled = true;
this.onControlStatusChange?.(true);
2026-08-14 09:02:52 +08:00
this.messageApi.success({ key: 'control', content: i18n.t('devices.fixed2ControlFixed2SuccessFixed2') });
break;
2026-05-20 15:12:51 +08:00
}
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;
}
2026-05-20 15:12:51 +08:00
case 'switchPermission': {
const platform = (msg.platform || '').toLowerCase();
if (!platform || platform === 'null') return;
2026-05-20 15:12:51 +08:00
this.requestDeviceId = msg.deviceId;
this.onPermissionRequest?.({
2026-08-14 09:02:52 +08:00
title: i18n.t('devices.s2'),
content: i18n.t('devices.platformRequestControl', { platform: msg.platform }),
2026-05-20 15:12:51 +08:00
});
break;
2026-05-20 15:12:51 +08:00
}
case 'detectionReport':
const { isSecure, obstacle } = msg.data;
if (isSecure) return;
const type = obstacle?.type;
const distance = (obstacle?.distance || 0).toFixed(2);
2026-08-14 09:02:52 +08:00
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',
2026-08-14 09:02:52 +08:00
content: i18n.t('devices.obstacleDetectedReport', { obstacle: types[type] ?? i18n.t('devices.unknownObject'), distance }),
duration: 3,
});
break;
2026-05-20 15:12:51 +08:00
case 'route_post':
console.log('route_post:', msg);
if (msg.deviceId) {
2026-09-08 17:17:59 +08:00
this.onRoutePost?.(msg);
}
break;
case 'device_status_changed':
this.deviceStatusChangedMsg?.(msg);
break;
case 'remoteControl':
break;
2026-05-20 15:12:51 +08:00
default:
break;
}
}
sendGamepad(state) {
if (!this.isControlled || !this.connectedDeviceSn) return;
wsManager.send({
2026-05-20 15:12:51 +08:00
type: 'remoteControl',
2026-05-21 09:47:24 +08:00
remoteData: state,
token: this.userInfo.token,
userName: this.userInfo.username,
});
}
}