diff --git a/.editorconfig b/.editorconfig index 3dce414..9d08a1a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,4 @@ indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true -trim_trailing_whitespace = true \ No newline at end of file +trim_trailing_whitespace = true diff --git a/src/main/ipcRouter.js b/src/main/ipcRouter.js index 3f753cd..a7362cc 100644 --- a/src/main/ipcRouter.js +++ b/src/main/ipcRouter.js @@ -3,12 +3,160 @@ import dgram from 'dgram' import net from 'net' import { IPC_EVENT } from '../renderer/src/common/ipcEvents.js' import log from 'electron-log' +import ReconnectManager from './reconnectManager.js' const TIMEOUT = 10000 // 10秒超时 const END_SEQUENCE = '\n\n' // 消息结束标志 // 全局保存所有TCP连接和相关信息 const tcpClients = new Map() // 保存待处理的请求,用于关联响应 const pendingRequests = new Map() +// 重连配置和状态管理 +const reconnectConfig = { + enabled: false, + interval: 5 // 默认5秒 +} + +// 创建重连管理器实例 +const reconnectManager = new ReconnectManager() + +// 配置重连管理器的事件处理 +reconnectManager.on('attempt-reconnect', (ip, callback) => { + // 获取重连信息 + const connectionData = reconnectManager.getReconnectStatus(ip) + if (!connectionData.config.enabled) { + callback(false, 'Reconnect disabled') + return + } + + log.info(`Executing reconnect attempt for ${ip}`) + const client = new net.Socket() + + // 设置连接超时为重连间隔的80%,确保在下次重连前完成 + const reconnectInterval = reconnectManager.getConfig().interval * 1000 + const connectionTimeout = Math.max(3000, Math.floor(reconnectInterval * 0.8)) + client.setTimeout(connectionTimeout) + + log.debug( + `Connection timeout set to ${connectionTimeout}ms (reconnect interval: ${reconnectInterval}ms)` + ) + + // 需要从某处获取端口和eventSender - 这里需要存储这些信息 + const storedConnectionData = tcpClients.get(ip) || tcpConnectionData.get(ip) + if (!storedConnectionData) { + callback(false, 'Connection data not found') + return + } + + const connectionInfo = { + client, + eventSender: storedConnectionData.eventSender, + ip: ip, + port: storedConnectionData.port + } + + client.connect(Number(storedConnectionData.port), ip, () => { + log.info(`Reconnected successfully to ${ip}:${storedConnectionData.port}`) + + // 连接成功,保存连接信息 + tcpClients.set(ip, connectionInfo) + + // 启动心跳检测 + startHeartbeatCheck(ip) + + // 通知渲染进程重连成功 + storedConnectionData.eventSender.send(IPC_EVENT.DEVICE_CONNECT_REPLY, { + success: true, + ip: ip, + reconnected: true + }) + + callback(true) + }) + + // 设置数据处理 + let buffer = '' + client.on('data', (data) => { + buffer += data.toString() + let index + while ((index = buffer.indexOf('\n')) !== -1) { + const line = buffer.slice(0, index) + buffer = buffer.slice(index + 1) + if (!line.trim()) continue + + let msg + try { + msg = JSON.parse(line) + } catch (e) { + log.error('TCP reconnect data parse error:', e) + continue + } + + if (!msg || !msg.command) continue + handleTcpResponse(ip, msg) + } + }) + + client.on('error', (err) => { + log.warn(`Reconnect attempt failed for ${ip}:`, err.message) + client.destroy() + callback(false, err) + }) + + client.on('timeout', () => { + log.warn(`Reconnect attempt timeout for ${ip}`) + client.destroy() + callback(false, new Error('Connection timeout')) + }) + + client.on('close', () => { + log.debug(`Reconnect attempt connection closed for ${ip}`) + // 连接关闭由心跳检测处理 + }) +}) + +// 处理重连状态更新 +reconnectManager.on('reconnect-status', (status) => { + // 通知所有渲染进程重连状态变化 + const storedConnectionData = tcpConnectionData.get(status.ip) + if (storedConnectionData && storedConnectionData.eventSender) { + log.info( + `Sending reconnect status update for ${status.ip}: ${status.isReconnecting ? 'reconnecting' : 'idle'}` + ) + storedConnectionData.eventSender.send(IPC_EVENT.RECONNECT_STATUS, { + ip: status.ip, + status: status.isReconnecting ? 'reconnecting' : 'idle', + attempts: status.attempts, + maxAttempts: status.maxAttempts, + lastError: status.lastError + }) + } else { + log.warn( + `No stored connection data found for ${status.ip}, cannot send reconnect status update` + ) + + // 如果找不到存储的连接数据,尝试从活动连接中获取 + const activeConnection = tcpClients.get(status.ip) + if (activeConnection && activeConnection.eventSender) { + log.info(`Using active connection data to send reconnect status for ${status.ip}`) + activeConnection.eventSender.send(IPC_EVENT.RECONNECT_STATUS, { + ip: status.ip, + status: status.isReconnecting ? 'reconnecting' : 'idle', + attempts: status.attempts, + maxAttempts: status.maxAttempts, + lastError: status.lastError + }) + } + } +}) + +// 存储连接数据以供重连使用 +const tcpConnectionData = new Map() // 存储连接参数 + +// 心跳检测相关配置 +const HEARTBEAT_INTERVAL = 5000 // 心跳检测间隔 5秒(设备每2秒发送,我们5秒检测一次) +const HEARTBEAT_TIMEOUT = 10000 // 心跳超时时间 10秒(设备每2秒发送,超过10秒没收到就认为断开) +const heartbeatTimers = new Map() // 保存每个IP的心跳检测定时器 +const lastHeartbeatTime = new Map() // 保存每个IP的最后心跳时间 // 记录IPC命令的通用函数(事件类型自动拼接) const logIPCCommand = (ip, command) => { @@ -20,6 +168,7 @@ export function registerIpRouter() { ipcMain.on(IPC_EVENT.DEVICE_SEARCH, searchDevice) ipcMain.on(IPC_EVENT.DEVICE_CONNECT, connectDevice) ipcMain.on(IPC_EVENT.DEVICE_DISCONNECT, disconnectDevice) + ipcMain.on(IPC_EVENT.RECONNECT_CONFIG, handleReconnectConfig) ipcMain.handle(IPC_EVENT.SENSORS_GET, sensorLoad) // 获取传感器 ipcMain.handle(IPC_EVENT.SENSORS_SET, sensorSet) // 传感器设置处理 ipcMain.handle(IPC_EVENT.IMAGE_SEND_TIME_GET, imageSendTimeGet) @@ -48,17 +197,44 @@ const searchDevice = (event) => { const BROADCAST_ADDR = '255.255.255.255' const udpClient = dgram.createSocket('udp4') + const UDP_SEARCH_TIMEOUT = 5000 // 5秒总超时时间 + const UDP_RESPONSE_DELAY = 1000 // 收到响应后等1秒收集更多设备 + let timer = null + let globalTimer = null const resultMap = new Map() + // 清理资源的函数 + const cleanup = () => { + if (timer) clearTimeout(timer) + if (globalTimer) clearTimeout(globalTimer) + if (!udpClient.destroyed) { + udpClient.close() + } + } + + // 发送结果并清理 + const sendResultsAndCleanup = () => { + const results = Array.from(resultMap.values()) + log.info(`UDP search completed, found ${results.length} devices`) + cleanup() + event.reply && event.reply(IPC_EVENT.DEVICE_SEARCH_REPLY, results) + } + + // 设置全局超时 - 无论如何5秒后结束搜索 + globalTimer = setTimeout(() => { + log.info('UDP search timeout - no devices found within timeout period') + sendResultsAndCleanup() + }, UDP_SEARCH_TIMEOUT) + udpClient.bind(() => { udpClient.setBroadcast(true) udpClient.send(message, 0, message.length, PORT, BROADCAST_ADDR, (err) => { if (err) { log.error('UDP send failed', err) - udpClient.close() + sendResultsAndCleanup() } else { - log.info('UDP send successful') + log.info('UDP broadcast sent, waiting for responses...') } }) }) @@ -69,21 +245,22 @@ const searchDevice = (event) => { from: rinfo.address, data: msg.toString() }) + log.info(`UDP response from ${rinfo.address}:`, msg.toString()) } catch (e) { log.error('UDP message parse error', e) } + + // 每次收到消息后,重置响应延迟定时器 if (timer) clearTimeout(timer) timer = setTimeout(() => { - udpClient.close() - log.info('UDP socket closed after timeout') - event.reply && event.reply(IPC_EVENT.DEVICE_SEARCH_REPLY, Array.from(resultMap.values())) - }, 1000) + log.info('UDP response collection timeout reached') + sendResultsAndCleanup() + }, UDP_RESPONSE_DELAY) }) udpClient.on('error', (err) => { log.error('UDP error:', err) - udpClient.close() - event.reply && event.reply(IPC_EVENT.DEVICE_SEARCH_REPLY, Array.from(resultMap.values())) + sendResultsAndCleanup() }) } @@ -111,6 +288,9 @@ const connectDevice = (event, { ip, port }) => { client.connect(Number(port), ip, () => { event.reply(IPC_EVENT.DEVICE_CONNECT_REPLY, { success: true, ip }) tcpClients.set(ip, connectionInfo) + + // 启动心跳检测 + startHeartbeatCheck(ip) }) let buffer = '' @@ -152,8 +332,23 @@ const connectDevice = (event, { ip, port }) => { client.on('close', () => { log.info(`TCP connection to ${ip} closed`) - tcpClients.delete(ip) - clearPendingRequestsByIp(ip) + const connectionInfo = tcpClients.get(ip) + if (connectionInfo) { + tcpClients.delete(ip) + clearPendingRequestsByIp(ip) + + // 只有在非主动断开的情况下才启动重连 + // 主动断开会先调用 disconnectDevice 函数 + if (reconnectConfig.enabled) { + log.info(`Connection lost to ${ip}, starting reconnect...`) + // 保存连接数据供重连使用 + tcpConnectionData.set(ip, { + port: port, + eventSender: connectionInfo.eventSender + }) + reconnectManager.startReconnect(ip, 'Connection lost') + } + } }) } @@ -161,8 +356,13 @@ const connectDevice = (event, { ip, port }) => { const disconnectDevice = (event, { ip }) => { const connectionInfo = tcpClients.get(ip) if (connectionInfo) { + // 停止重连和心跳检测 + reconnectManager.stopReconnect(ip) + stopHeartbeatCheck(ip) + connectionInfo.client.destroy() tcpClients.delete(ip) + tcpConnectionData.delete(ip) // 清除连接数据 clearPendingRequestsByIp(ip) event.reply(IPC_EVENT.DEVICE_DISCONNECT_REPLY, { success: true }) } else { @@ -382,7 +582,9 @@ const handleTcpResponse = async (ip, msg) => { } break case IPC_EVENT.HEARTBEAT_REPLY: - // 心跳处理 + // 更新心跳时间戳(设备每2秒发送心跳包) + lastHeartbeatTime.set(ip, Date.now()) + // 心跳包不记录到日志,避免日志过多 break default: @@ -1150,3 +1352,98 @@ const clearPendingRequestsByIp = (ip) => { const delay = () => { return new Promise((resolve) => setTimeout(resolve, 1000)) } + +// 处理重连配置 +const handleReconnectConfig = (event, { enabled, interval }) => { + reconnectConfig.enabled = enabled + reconnectConfig.interval = Math.max(4, interval) // 最小4秒 + + // 更新重连管理器配置 + reconnectManager.updateConfig({ + enabled, + interval: reconnectConfig.interval + }) + + log.info('Reconnect config updated:', { enabled, interval: reconnectConfig.interval }) +} + +// 启动心跳检测 +const startHeartbeatCheck = (ip) => { + // 清除之前的心跳检测定时器 + stopHeartbeatCheck(ip) + + // 初始化心跳时间 + lastHeartbeatTime.set(ip, Date.now()) + + // 设置心跳检测定时器 + const timer = setInterval(() => { + checkHeartbeat(ip) + }, HEARTBEAT_INTERVAL) + + heartbeatTimers.set(ip, timer) + log.info(`Started heartbeat check for ${ip}`) +} + +// 检查心跳超时 +const checkHeartbeat = (ip) => { + const lastTime = lastHeartbeatTime.get(ip) + const currentTime = Date.now() + + if (!lastTime) { + log.warn(`No heartbeat recorded for ${ip}`) + return + } + + const timeSinceLastHeartbeat = currentTime - lastTime + + if (timeSinceLastHeartbeat > HEARTBEAT_TIMEOUT) { + log.warn( + `Heartbeat timeout for ${ip}: ${timeSinceLastHeartbeat}ms since last heartbeat (timeout: ${HEARTBEAT_TIMEOUT}ms)` + ) + + // 心跳超时,主动断开连接并启动重连 + const connectionInfo = tcpClients.get(ip) + if (connectionInfo) { + log.info(`Force disconnecting ${ip} due to heartbeat timeout`) + + // 停止心跳检测 + stopHeartbeatCheck(ip) + + // 销毁连接 + connectionInfo.client.destroy() + tcpClients.delete(ip) + clearPendingRequestsByIp(ip) + + // 通知渲染进程连接断开 + connectionInfo.eventSender.send(IPC_EVENT.DEVICE_DISCONNECT_REPLY, { + success: true, + reason: 'heartbeat_timeout' + }) + + // 启动重连(如果启用) + if (reconnectConfig.enabled) { + log.info(`Connection lost to ${ip} due to heartbeat timeout, starting reconnect...`) + // 保存连接数据供重连使用 + tcpConnectionData.set(ip, { + port: connectionInfo.port, + eventSender: connectionInfo.eventSender + }) + reconnectManager.startReconnect(ip, 'Heartbeat timeout') + } + } + } else { + // 心跳正常,记录调试信息(可选) + log.debug(`Heartbeat OK for ${ip}: ${timeSinceLastHeartbeat}ms since last heartbeat`) + } +} + +// 停止心跳检测 +const stopHeartbeatCheck = (ip) => { + const timer = heartbeatTimers.get(ip) + if (timer) { + clearInterval(timer) + heartbeatTimers.delete(ip) + } + lastHeartbeatTime.delete(ip) + log.info(`Stopped heartbeat check for ${ip}`) +} diff --git a/src/main/reconnectManager.js b/src/main/reconnectManager.js new file mode 100644 index 0000000..fdd737e --- /dev/null +++ b/src/main/reconnectManager.js @@ -0,0 +1,221 @@ +import { EventEmitter } from 'events' +const log = require('electron-log') + +class ReconnectManager extends EventEmitter { + constructor() { + super() + this.reconnectTimers = new Map() // IP -> timer对象 + this.reconnectAttempts = new Map() // IP -> 当前重连次数 + this.reconnectConfig = { + enabled: false, + interval: 5, // 秒 + maxAttempts: 10 + } + this.isReconnecting = new Map() // IP -> boolean,防止重复重连 + } + + // 更新重连配置 + updateConfig(config) { + this.reconnectConfig = { + ...this.reconnectConfig, + ...config, + interval: Math.max(4, config.interval || this.reconnectConfig.interval) // 最小4秒 + } + log.info('Reconnect config updated:', this.reconnectConfig) + } + + // 启动重连 + startReconnect(ip, reason = 'Connection lost') { + if (!this.reconnectConfig.enabled) { + log.info(`Reconnect disabled for ${ip}`) + return + } + + // 防止重复启动重连 + if (this.isReconnecting.get(ip)) { + log.warn(`Reconnect already in progress for ${ip}`) + return + } + + this.isReconnecting.set(ip, true) + + // 如果没有当前重连次数,初始化为0 + if (!this.reconnectAttempts.has(ip)) { + this.reconnectAttempts.set(ip, 0) + } + + log.info(`${reason}, starting reconnect for ${ip}...`) + + // 发送重连状态更新 + this.emit('reconnect-status', { + ip, + isReconnecting: true + }) + + // 设置重连定时器 + this.scheduleNextAttempt(ip) + } + + // 安排下次重连尝试 + scheduleNextAttempt(ip) { + // 递增重连次数 + const currentAttempts = this.reconnectAttempts.get(ip) || 0 + const newAttempts = currentAttempts + 1 + this.reconnectAttempts.set(ip, newAttempts) + + if (newAttempts > this.reconnectConfig.maxAttempts) { + log.error( + `Max reconnect attempts reached for ${ip} (${newAttempts}/${this.reconnectConfig.maxAttempts})` + ) + this.stopReconnect(ip, 'Max attempts reached') + return + } + + log.info( + `Scheduling reconnect attempt ${newAttempts}/${this.reconnectConfig.maxAttempts} for ${ip}` + ) + + const intervalMs = this.reconnectConfig.interval * 1000 + log.info(`Reconnect timer set for ${ip} in ${this.reconnectConfig.interval}s (${intervalMs}ms)`) + + const timer = setTimeout(() => { + log.info(`Reconnect timer fired for ${ip}, executing attempt ${newAttempts}`) + this.executeReconnectAttempt(ip) + }, intervalMs) + + // 清除之前的定时器 + this.clearTimer(ip) + this.reconnectTimers.set(ip, timer) + } + + // 执行重连尝试 + executeReconnectAttempt(ip) { + const attempts = this.reconnectAttempts.get(ip) || 0 + log.info( + `Executing reconnect attempt ${attempts}/${this.reconnectConfig.maxAttempts} for ${ip}` + ) + + // 发出重连请求事件 + this.emit('attempt-reconnect', ip, (success, error) => { + if (success) { + log.info(`Reconnect attempt ${attempts} successful for ${ip}`) + this.onReconnectSuccess(ip) + } else { + log.warn(`Reconnect attempt ${attempts} failed for ${ip}: ${error?.message || error}`) + this.onReconnectFailure(ip, error) + } + }) + } + + // 重连成功处理 + onReconnectSuccess(ip) { + const attempts = this.reconnectAttempts.get(ip) || 0 + log.info(`Reconnect attempt ${attempts} successful for ${ip}, clearing reconnect state`) + + // 停止重连 + this.stopReconnect(ip, 'Reconnect successful') + } + + // 重连失败处理 + onReconnectFailure(ip, error) { + const attempts = this.reconnectAttempts.get(ip) || 0 + + log.warn( + `Reconnect attempt ${attempts} failed for ${ip}, scheduling next attempt in ${this.reconnectConfig.interval}s`, + error + ) + + // 发送重连状态更新 + this.emit('reconnect-status', { + ip, + isReconnecting: true + }) + + // 安排下次重连 + this.scheduleNextAttempt(ip) + } + + // 停止重连 + stopReconnect(ip, reason = 'Manual stop') { + log.info(`Stopping reconnect for ${ip}: ${reason}`) + + this.clearTimer(ip) + this.reconnectAttempts.delete(ip) + this.isReconnecting.set(ip, false) + + // 发送重连状态更新 + this.emit('reconnect-status', { + ip, + isReconnecting: false + }) + } + + // 清除重连定时器 + clearTimer(ip) { + const timer = this.reconnectTimers.get(ip) + if (timer) { + clearTimeout(timer) + this.reconnectTimers.delete(ip) + log.debug(`Cleared reconnect timer for ${ip}`) + } + } + + // 重置重连计数 + resetAttempts(ip) { + this.reconnectAttempts.set(ip, 0) + log.debug(`Reset reconnect attempts for ${ip}`) + } + + // 检查是否正在重连 + isReconnectInProgress(ip) { + return this.isReconnecting.get(ip) || false + } + + // 获取重连状态 + getReconnectStatus(ip) { + return { + isReconnecting: this.isReconnecting.get(ip) || false, + attempts: this.reconnectAttempts.get(ip) || 0, + maxAttempts: this.reconnectConfig.maxAttempts, + config: this.reconnectConfig + } + } + + // 获取所有重连状态 + getAllReconnectStatus() { + const status = {} + for (const [ip] of this.isReconnecting) { + status[ip] = this.getReconnectStatus(ip) + } + return status + } + + // 清理指定IP的所有重连状态 + cleanup(ip) { + log.info(`Cleaning up reconnect state for ${ip}`) + this.clearTimer(ip) + this.reconnectAttempts.delete(ip) + this.isReconnecting.delete(ip) + } + + // 清理所有重连状态 + cleanupAll() { + log.info('Cleaning up all reconnect states') + + // 清除所有定时器 + for (const [ip] of this.reconnectTimers) { + this.clearTimer(ip) + } + + // 清除所有状态 + this.reconnectAttempts.clear() + this.isReconnecting.clear() + } + + // 获取配置 + getConfig() { + return { ...this.reconnectConfig } + } +} + +export default ReconnectManager diff --git a/src/renderer/src/common/ipcEvents.js b/src/renderer/src/common/ipcEvents.js index 1b69864..1290c49 100644 --- a/src/renderer/src/common/ipcEvents.js +++ b/src/renderer/src/common/ipcEvents.js @@ -33,6 +33,10 @@ export const IPC_EVENT = { THRESHOLD_SET: 'threshold:set', INVALID_DATA_COUNT_SET: 'invalidDataCount:set', + // 重连相关 + RECONNECT_CONFIG: 'reconnect:config', + RECONNECT_STATUS: 'reconnect:status', + // 自动更新相关的value不要乱改,定义在这里是为了方便,这些都是标准的api事件名 UPDATE_AVAILABLE: 'update-available', UPDATE_NOT_AVAILABLE: 'update-not-available', diff --git a/src/renderer/src/components/DeflectionCollection/DeflectionCollection.jsx b/src/renderer/src/components/DeflectionCollection/DeflectionCollection.jsx index 9cb1c36..bfe1bec 100644 --- a/src/renderer/src/components/DeflectionCollection/DeflectionCollection.jsx +++ b/src/renderer/src/components/DeflectionCollection/DeflectionCollection.jsx @@ -22,6 +22,8 @@ function DeflectionCollection() { // 获取设备连接状态 const connectedDevice = useDeviceStore((state) => state.connectedDevice) + const alarmEnabled = useDeviceStore((state) => state.alarmEnabled) + const alarmLimits = useDeviceStore((state) => state.alarmLimits) // 为每个传感器生成颜色 const targetColors = [ @@ -96,35 +98,93 @@ function DeflectionCollection() { // 生成Y方向数据 const dataY = { labels: timeLabels, - datasets: allSensors.map((sensor) => ({ - label: `测点${sensor.pos}`, - data: sensorDataHistory.map((dataPoint) => { - const sensorData = dataPoint.sensors.find((s) => s.pos === sensor.pos) - return sensorData ? Number(sensorData.yReal) : null - }), - borderColor: sensor.color, - backgroundColor: sensor.color.replace('1)', '0.2)'), - tension: 0, - pointRadius: 3, - pointHoverRadius: 5 - })) + datasets: [ + // 传感器数据线 + ...allSensors.map((sensor) => ({ + label: `测点${sensor.pos}`, + data: sensorDataHistory.map((dataPoint) => { + const sensorData = dataPoint.sensors.find((s) => s.pos === sensor.pos) + return sensorData ? Number(sensorData.yReal) : null + }), + borderColor: sensor.color, + backgroundColor: sensor.color.replace('1)', '0.2)'), + tension: 0, + pointRadius: 3, + pointHoverRadius: 5, + borderDash: [] // 实线 + })), + // 报警上下限线 + ...(alarmEnabled + ? [ + { + label: 'Y轴上限', + data: timeLabels.map(() => alarmLimits.yUpper), + borderColor: 'rgba(255, 0, 0, 0.8)', + backgroundColor: 'rgba(255, 0, 0, 0.1)', + borderDash: [5, 5], // 虚线 + pointRadius: 0, + tension: 0, + fill: false + }, + { + label: 'Y轴下限', + data: timeLabels.map(() => alarmLimits.yLower), + borderColor: 'rgba(255, 0, 0, 0.8)', + backgroundColor: 'rgba(255, 0, 0, 0.1)', + borderDash: [5, 5], // 虚线 + pointRadius: 0, + tension: 0, + fill: false + } + ] + : []) + ] } // 生成X方向数据 const dataX = { labels: timeLabels, - datasets: allSensors.map((sensor) => ({ - label: `测点${sensor.pos}`, - data: sensorDataHistory.map((dataPoint) => { - const sensorData = dataPoint.sensors.find((s) => s.pos === sensor.pos) - return sensorData ? Number(sensorData.xReal) : null - }), - borderColor: sensor.color, - backgroundColor: sensor.color.replace('1)', '0.2)'), - tension: 0, - pointRadius: 3, - pointHoverRadius: 5 - })) + datasets: [ + // 传感器数据线 + ...allSensors.map((sensor) => ({ + label: `测点${sensor.pos}`, + data: sensorDataHistory.map((dataPoint) => { + const sensorData = dataPoint.sensors.find((s) => s.pos === sensor.pos) + return sensorData ? Number(sensorData.xReal) : null + }), + borderColor: sensor.color, + backgroundColor: sensor.color.replace('1)', '0.2)'), + tension: 0, + pointRadius: 3, + pointHoverRadius: 5, + borderDash: [] // 实线 + })), + // 报警上下限线 + ...(alarmEnabled + ? [ + { + label: 'X轴上限', + data: timeLabels.map(() => alarmLimits.xUpper), + borderColor: 'rgba(255, 0, 0, 0.8)', + backgroundColor: 'rgba(255, 0, 0, 0.1)', + borderDash: [5, 5], // 虚线 + pointRadius: 0, + tension: 0, + fill: false + }, + { + label: 'X轴下限', + data: timeLabels.map(() => alarmLimits.xLower), + borderColor: 'rgba(255, 0, 0, 0.8)', + backgroundColor: 'rgba(255, 0, 0, 0.1)', + borderDash: [5, 5], // 虚线 + pointRadius: 0, + tension: 0, + fill: false + } + ] + : []) + ] } const options = { diff --git a/src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.jsx b/src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.jsx index 9a76014..46f13d1 100644 --- a/src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.jsx +++ b/src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.jsx @@ -422,6 +422,7 @@ function MeasurementPointSetting() { value={formData.location} onChange={(value) => handleFormChange('location', value)} style={{ flex: 1 }} + disabled={!connectedDevice} /> @@ -432,6 +433,7 @@ function MeasurementPointSetting() { value={formData.description} onChange={(e) => handleFormChange('description', e.target.value)} style={{ flex: 1 }} + disabled={!connectedDevice} /> @@ -453,6 +455,7 @@ function MeasurementPointSetting() { value={formData.baseTarget} onChange={(value) => handleFormChange('baseTarget', value)} style={{ flex: 1 }} + disabled={!connectedDevice} options={[ { value: 'y', label: 'y' }, { value: 'n', label: 'n' } @@ -471,6 +474,7 @@ function MeasurementPointSetting() { className={styles.numberInput} value={rectangleData?.x || 0} style={{ flex: 1 }} + disabled={!connectedDevice} /> @@ -479,6 +483,7 @@ function MeasurementPointSetting() { className={styles.numberInput} value={rectangleData?.width || 0} style={{ flex: 1 }} + disabled={!connectedDevice} /> @@ -490,6 +495,7 @@ function MeasurementPointSetting() { className={styles.numberInput} value={rectangleData?.y || 0} style={{ flex: 1 }} + disabled={!connectedDevice} /> @@ -498,6 +504,7 @@ function MeasurementPointSetting() { className={styles.numberInput} value={rectangleData?.height || 0} style={{ flex: 1 }} + disabled={!connectedDevice} /> @@ -518,6 +525,7 @@ function MeasurementPointSetting() { style={{ flex: 1 }} status={!lensDistance || lensDistance <= 0 ? 'error' : ''} placeholder="请输入大于0的数值" + disabled={!connectedDevice} /> @@ -529,6 +537,7 @@ function MeasurementPointSetting() { onChange={handleMeasureDistanceChange} precision={4} style={{ flex: 1 }} + disabled={!connectedDevice} /> @@ -551,6 +560,7 @@ function MeasurementPointSetting() { type: 'radio', selectedRowKeys: selectedSensorKey ? [selectedSensorKey] : [], onChange: (selectedRowKeys) => { + if (!connectedDevice) return // 只允许选择顶级传感器行(不包含子行) const selectedKey = selectedRowKeys[0] if (selectedKey && !selectedKey.includes('-')) { @@ -559,7 +569,7 @@ function MeasurementPointSetting() { }, getCheckboxProps: (record) => ({ // 只有顶级传感器行可以选择,子行不显示选择框 - disabled: record.key.includes('-') + disabled: !connectedDevice || record.key.includes('-') }), hideSelectAll: true, renderCell: (checked, record, index, originNode) => { @@ -572,13 +582,14 @@ function MeasurementPointSetting() { }} onRow={(record) => ({ onClick: () => { + if (!connectedDevice) return // 点击行时选中,只允许选择顶级传感器行 if (!record.key.includes('-')) { handleSensorSelect(record.key) } }, style: { - cursor: record.key.includes('-') ? 'default' : 'pointer', + cursor: !connectedDevice || record.key.includes('-') ? 'default' : 'pointer', backgroundColor: selectedSensorKey === record.key ? '#e6f7ff' : 'transparent' } })} @@ -587,13 +598,28 @@ function MeasurementPointSetting() { {/* 操作按钮 */} - diff --git a/src/renderer/src/components/SystemSettings/SystemSettings.jsx b/src/renderer/src/components/SystemSettings/SystemSettings.jsx index bd71e4e..22490dc 100644 --- a/src/renderer/src/components/SystemSettings/SystemSettings.jsx +++ b/src/renderer/src/components/SystemSettings/SystemSettings.jsx @@ -8,21 +8,61 @@ import { EyeOutlined, EditOutlined } from '@ant-design/icons' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { IPC_EVENT } from '../../common/ipcEvents' import useDeviceStore from '../../stores/deviceStore' function SystemSettings() { // 状态管理 - const [selectedParam, setSelectedParam] = useState('') + const [selectedParam, setSelectedParam] = useState('imageSendTime') const [paramValue, setParamValue] = useState('') const [loading, setLoading] = useState(false) const [settingLoading, setSettingLoading] = useState(false) const [imageControlLoading, setImageControlLoading] = useState(false) const [clearZeroLoading, setClearZeroLoading] = useState(false) - // 获取设备连接状态 + // 获取设备连接状态和重连配置 const connectedDevice = useDeviceStore((state) => state.connectedDevice) + const reconnectEnabled = useDeviceStore((state) => state.reconnectEnabled) + const reconnectInterval = useDeviceStore((state) => state.reconnectInterval) + const isReconnecting = useDeviceStore((state) => state.isReconnecting) + const alarmEnabled = useDeviceStore((state) => state.alarmEnabled) + const alarmLimits = useDeviceStore((state) => state.alarmLimits) + const setReconnectEnabled = useDeviceStore((state) => state.setReconnectEnabled) + const setReconnectInterval = useDeviceStore((state) => state.setReconnectInterval) + const setReconnecting = useDeviceStore((state) => state.setReconnecting) + const setAlarmEnabled = useDeviceStore((state) => state.setAlarmEnabled) + const setAlarmLimits = useDeviceStore((state) => state.setAlarmLimits) + + // 监听重连状态更新 + useEffect(() => { + const reconnectStatusHandler = (event, result) => { + const { status } = result + console.log('Received reconnect status:', result) + + switch (status) { + case 'reconnecting': + setReconnecting(true) + break + + case 'idle': + case 'connected': + case 'failed': + setReconnecting(false) + break + + default: + console.warn('Unknown reconnect status:', status) + break + } + } + + window.electron.ipcRenderer.on(IPC_EVENT.RECONNECT_STATUS, reconnectStatusHandler) + + return () => { + window.electron.ipcRenderer.removeListener(IPC_EVENT.RECONNECT_STATUS, reconnectStatusHandler) + } + }, [setReconnecting]) // 读取参数函数 const handleReadParam = async () => { @@ -222,6 +262,7 @@ function SystemSettings() { placeholder="请选择基本参数" value={selectedParam} onChange={setSelectedParam} + disabled={!connectedDevice} options={[ { value: 'imageSendTime', label: 'imageSendTime' }, { value: 'zeroCount', label: 'zeroCount' }, @@ -237,6 +278,7 @@ function SystemSettings() { value={paramValue} onChange={(e) => setParamValue(e.target.value)} placeholder="参数值" + disabled={!connectedDevice} /> - + + - + - 实时数据 - 报警数据 + + 实时数据 + + 报警数据 ) diff --git a/src/renderer/src/stores/deviceStore.js b/src/renderer/src/stores/deviceStore.js index 441a48d..43af77a 100644 --- a/src/renderer/src/stores/deviceStore.js +++ b/src/renderer/src/stores/deviceStore.js @@ -10,6 +10,22 @@ const useDeviceStore = create((set) => ({ // 连接状态 isConnecting: false, + // 重连配置 + reconnectEnabled: false, + reconnectInterval: 5, // 默认5秒 + + // 重连状态 + isReconnecting: false, + + // 报警配置 + alarmEnabled: false, + alarmLimits: { + xUpper: 15, + xLower: -15, + yUpper: 15, + yLower: -15 + }, + // 设置连接的设备 setConnectedDevice: (device) => set({ connectedDevice: device }), @@ -22,6 +38,17 @@ const useDeviceStore = create((set) => ({ // 设置连接状态 setConnecting: (status) => set({ isConnecting: status }), + // 重连配置管理 + setReconnectEnabled: (enabled) => set({ reconnectEnabled: enabled }), + setReconnectInterval: (interval) => set({ reconnectInterval: Math.max(4, interval) }), // 最小4秒 + + // 重连状态管理 + setReconnecting: (status) => set({ isReconnecting: status }), + + // 报警配置管理 + setAlarmEnabled: (enabled) => set({ alarmEnabled: enabled }), + setAlarmLimits: (limits) => set({ alarmLimits: limits }), + // 获取当前连接的IP getConnectedIp: () => { const state = useDeviceStore.getState()