Browse Source

feat: 添加设备管理功能,集成设备连接状态和设备列表管理,重构主进程与设备的TCP通信。

master
cles 2 weeks ago
parent
commit
3a5389b1fb
  1. 150
      src/main/ipcRouter.js
  2. 7
      src/renderer/src/common/ipcEvents.js
  3. 8
      src/renderer/src/components/ImageCollection/ImageCollection.jsx
  4. 84
      src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.jsx
  5. 25
      src/renderer/src/components/SiderHeader/SiderHeader.jsx
  6. 32
      src/renderer/src/stores/deviceStore.js

150
src/main/ipcRouter.js

@ -3,12 +3,17 @@ import dgram from 'dgram'
import net from 'net'
import { IPC_EVENT } from '../renderer/src/common/ipcEvents.js'
import fs from 'fs'
// 全局保存所有TCP连接
// 全局保存所有TCP连接和相关信息
const tcpClients = new Map()
// 保存待处理的请求,用于关联响应
const pendingRequests = new Map()
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.DEVICE_SEARCH, searchDevice)
ipcMain.on(IPC_EVENT.DEVICE_CONNECT, connectDevice)
ipcMain.on(IPC_EVENT.DEVICE_DISCONNECT, disconnectDevice)
ipcMain.handle(IPC_EVENT.SENSORS_GET, sensorLoad) // 改为handle,支持异步返回
}
const searchDevice = (event) => {
@ -34,7 +39,6 @@ const searchDevice = (event) => {
udpClient.on('message', (msg, rinfo) => {
try {
// 以 IP 为 key,自动去重
resultMap.set(rinfo.address, {
from: rinfo.address,
data: msg.toString()
@ -42,20 +46,17 @@ const searchDevice = (event) => {
} catch (e) {
console.error('parse UDP message failed:', e)
}
// 每收到一条消息就重置定时器
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
udpClient.close()
console.log('UDP socket closed after timeout')
// 关闭后统一回复所有结果(已去重)
event.reply && event.reply(IPC_EVENT.DEVICE_SEARCH_REPLY, Array.from(resultMap.values()))
}, 1000) // 1秒内没有新消息就关闭
}, 1000)
})
udpClient.on('error', (err) => {
console.error('UDP error:', err)
udpClient.close()
// 出错时也可以回复已收到的内容
event.reply && event.reply(IPC_EVENT.DEVICE_SEARCH_REPLY, Array.from(resultMap.values()))
})
}
@ -69,10 +70,20 @@ const connectDevice = (event, { ip, port }) => {
event.reply(IPC_EVENT.DEVICE_CONNECT_REPLY, { success: false, error: '已连接' })
return
}
const client = new net.Socket()
// 保存连接信息,包括event sender用于后续通信
const connectionInfo = {
client,
eventSender: event.sender,
ip,
port
}
client.connect(Number(port), ip, () => {
event.reply(IPC_EVENT.DEVICE_CONNECT_REPLY, { success: true, ip })
tcpClients.set(ip, client)
tcpClients.set(ip, connectionInfo)
})
let buffer = ''
@ -84,50 +95,137 @@ const connectDevice = (event, { ip, port }) => {
const line = buffer.slice(0, index)
buffer = buffer.slice(index + 1)
if (!line.trim()) continue
let msg
try {
msg = JSON.parse(line)
console.log(msg.command)
console.log('Received command:', `${msg.command}:${msg.type}`)
} catch (e) {
console.error('TCP data parse error:', e)
fs.appendFileSync('error_log.txt', line + '\n')
continue
}
if (!msg || !msg.command) {
console.log('invalid msg format:', msg)
continue
}
switch (msg.command) {
case 'result':
event.sender.send(IPC_EVENT.SENSOR_DATA, { ip, ...msg })
break
case 'image':
event.sender.send(IPC_EVENT.IMAGE_DATA, { ip, ...msg })
break
case 'heartbeat':
break
default:
console.warn('unknow command type:', msg.command)
}
// 处理TCP响应
handleTcpResponse(ip, msg)
}
})
client.on('error', (err) => {
event.reply(IPC_EVENT.DEVICE_CONNECT_REPLY, { success: false, error: err.message })
client.destroy()
tcpClients.delete(ip)
// 清理该IP的所有待处理请求
clearPendingRequestsByIp(ip)
})
client.on('close', () => {
tcpClients.delete(ip)
clearPendingRequestsByIp(ip)
})
}
const disconnectDevice = (event, { ip }) => {
const client = tcpClients.get(ip)
if (client) {
client.destroy()
const connectionInfo = tcpClients.get(ip)
if (connectionInfo) {
connectionInfo.client.destroy()
tcpClients.delete(ip)
clearPendingRequestsByIp(ip)
event.reply(IPC_EVENT.DEVICE_DISCONNECT_REPLY, { success: true })
} else {
event.reply(IPC_EVENT.DEVICE_DISCONNECT_REPLY, { success: false, error: '未连接' })
}
}
// 处理TCP响应
const handleTcpResponse = (ip, msg) => {
const connectionInfo = tcpClients.get(ip)
if (!connectionInfo) return
switch (`${msg.command}:${msg.type}`) {
case IPC_EVENT.RESULT_REPLY:
connectionInfo.eventSender.send(IPC_EVENT.RESULT_REPLY, { ip, ...msg })
break
case IPC_EVENT.IMAGE_REPLY:
connectionInfo.eventSender.send(IPC_EVENT.IMAGE_REPLY, { ip, ...msg })
break
case IPC_EVENT.SENSORS_GET:
{
const sensorRequest = pendingRequests.get(`${ip}${IPC_EVENT.SENSORS_GET}`)
if (sensorRequest) {
// 响应传感器加载请求
sensorRequest.resolve({ success: true, data: msg })
pendingRequests.delete(`${ip}${IPC_EVENT.SENSORS_GET}`)
}
}
break
case IPC_EVENT.HEARTBEAT_REPLY:
// 心跳处理
break
default:
console.warn('unknown command:', `${msg.command}:${msg.type}`)
}
}
// 传感器加载 - 改为异步处理
const sensorLoad = async (event, { ip }) => {
return new Promise((resolve, reject) => {
const connectionInfo = tcpClients.get(ip)
if (!connectionInfo) {
resolve({ success: false, error: '设备未连接' })
return
}
// 生成请求ID并保存待处理请求
const requestKey = `${ip}${IPC_EVENT.SENSORS_GET}`
// 设置超时
const timeout = setTimeout(() => {
pendingRequests.delete(requestKey)
resolve({ success: false, error: '请求超时' })
}, 10000) // 10秒超时
// 保存请求信息
pendingRequests.set(requestKey, {
resolve: (result) => {
clearTimeout(timeout)
resolve(result)
},
reject: (error) => {
clearTimeout(timeout)
reject(error)
},
timestamp: Date.now()
})
// 发送传感器加载命令
const command = { command: 'sensors', type: 'get', values: '' }
const message = JSON.stringify(command) + '\n\n'
connectionInfo.client.write(message, (err) => {
if (err) {
pendingRequests.delete(requestKey)
clearTimeout(timeout)
resolve({ success: false, error: err.message })
}
})
})
}
// 清理指定IP的所有待处理请求
const clearPendingRequestsByIp = (ip) => {
const keysToDelete = []
for (const [key, request] of pendingRequests) {
if (key.startsWith(`${ip}_`)) {
request.resolve({ success: false, error: '连接已断开' })
keysToDelete.push(key)
}
}
keysToDelete.forEach((key) => pendingRequests.delete(key))
}

7
src/renderer/src/common/ipcEvents.js

@ -7,7 +7,8 @@ export const IPC_EVENT = {
DEVICE_DISCONNECT: 'device:disconnect',
DEVICE_DISCONNECT_REPLY: 'device:disconnect:reply',
SENSOR_DATA: 'sensor:data', // 传感器数据
IMAGE_DATA: 'image:data', // 图像数据
HEARTBEAT: 'heartbeat' // 心跳包
RESULT_REPLY: 'result:reply', // 传感器数据
IMAGE_REPLY: 'image:reply', // 图像数据
HEARTBEAT_REPLY: 'heartbeat:reply', // 心跳包
SENSORS_GET: 'sensors:get' // 加载传感器配置
}

8
src/renderer/src/components/ImageCollection/ImageCollection.jsx

@ -31,10 +31,6 @@ function ImagePreview() {
height: rectangleData.height * imageScale
}
setRectangle(displayRect)
console.log('从store更新矩形显示:', {
原始数据: rectangleData,
显示坐标: displayRect
})
} else if (!rectangleData) {
setRectangle(null)
}
@ -250,9 +246,9 @@ function ImagePreview() {
const src = `data:image/png;base64,${base64}`
setImgSrc(src)
}
window.electron.ipcRenderer.on(IPC_EVENT.IMAGE_DATA, handler)
window.electron.ipcRenderer.on(IPC_EVENT.IMAGE_REPLY, handler)
return () => {
window.electron.ipcRenderer.removeListener(IPC_EVENT.IMAGE_DATA, handler)
window.electron.ipcRenderer.removeListener(IPC_EVENT.IMAGE_REPLY, handler)
}
}, [])

84
src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.jsx

@ -1,5 +1,5 @@
import styles from './MeasurementPointSetting.module.css'
import { Flex, Input, Select, InputNumber, Button, Table, Tooltip, Modal } from 'antd'
import { Flex, Input, Select, InputNumber, Button, Table, Tooltip, Modal, message } from 'antd'
import {
PlusOutlined,
MinusOutlined,
@ -9,13 +9,18 @@ import {
BoxPlotFilled
} from '@ant-design/icons'
import useRectangleStore from '../../stores/rectangleStore'
import useDeviceStore from '../../stores/deviceStore'
import { useState } from 'react'
import { IPC_EVENT } from '../../common/ipcEvents'
function MeasurementPointSetting() {
// Zustand store
const rectangleData = useRectangleStore((state) => state.rectangleData)
const setRectangleData = useRectangleStore((state) => state.setRectangleData)
// store
const connectedDevice = useDeviceStore((state) => state.connectedDevice)
//
const [formData, setFormData] = useState({
location: 1, //
@ -64,8 +69,6 @@ function MeasurementPointSetting() {
width: Number(wData.value) || 0,
height: Number(hData.value) || 0
}
console.log('从传感器数据更新矩形:', rectangleInfo)
setRectangleData(rectangleInfo)
} else {
setRectangleData(null)
@ -240,9 +243,80 @@ function MeasurementPointSetting() {
})
}
const handleLoad = () => {}
const handleLoad = async () => {
try {
// storeIP
if (!connectedDevice || !connectedDevice.ip) {
message.warning('请先连接设备后再加载传感器数据!')
return
}
const deviceIp = connectedDevice.ip
//
const result = await window.electron.ipcRenderer.invoke(IPC_EVENT.SENSORS_GET, {
ip: deviceIp
})
if (result.success) {
console.log('传感器数据加载成功:', result.data)
message.success('传感器数据已成功加载!')
} else {
console.error('传感器数据加载失败:', result.error)
message.error(`传感器数据加载失败:${result.error}`)
}
} catch (error) {
console.error('调用传感器加载失败:', error)
message.error(`调用传感器加载失败:${error.message}`)
}
}
const handleSet = () => {
//
if (!sensorList || sensorList.length === 0) {
message.warning('没有传感器数据,请先添加传感器!')
return
}
//
if (!connectedDevice || !connectedDevice.ip) {
message.warning('请先连接设备后再设置传感器!')
return
}
//
const collectedData = sensorList.map((sensor) => {
// children
const children = sensor.children || []
//
const positionItem = children.find((item) => item.name === '测点位置')
const descriptionItem = children.find((item) => item.name === '测点描述')
const coefficientItem = children.find((item) => item.name === '计算系数')
const targetItem = children.find((item) => item.name === '基准标靶')
// children
const targetChildren = targetItem?.children || []
const xItem = targetChildren.find((item) => item.name === 'x')
const yItem = targetChildren.find((item) => item.name === 'y')
const wItem = targetChildren.find((item) => item.name === 'w')
const hItem = targetChildren.find((item) => item.name === 'h')
return {
arg: String(coefficientItem?.value), //
des: String(descriptionItem?.value), //
pos: String(positionItem?.value), //
tar: String(targetItem?.value), //
w: String(wItem?.value), //
h: String(hItem?.value), //
x: String(xItem?.value), // x
y: String(yItem?.value) // y
}
})
const handleSet = () => {}
console.log('收集到的传感器数据:', collectedData)
console.log('格式化后的JSON:', JSON.stringify(collectedData, null, 2))
}
return (
<Flex vertical className={styles.container}>

25
src/renderer/src/components/SiderHeader/SiderHeader.jsx

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import styles from './SiderHeader.module.css'
import { Flex, Select, Button, Input, Spin, message } from 'antd'
import { IPC_EVENT } from '../../common/ipcEvents.js'
import useDeviceStore from '../../stores/deviceStore'
import {
VideoCameraFilled,
GoldFilled,
@ -20,16 +21,25 @@ function SiderHeader({ showSystemSettings = true }) {
const [devicePort, setDevicePort] = useState('2230')
const [connected, setConnected] = useState(null) // IP
// 使store
const setConnectedDevice = useDeviceStore((state) => state.setConnectedDevice)
const clearConnectedDevice = useDeviceStore((state) => state.clearConnectedDevice)
const setDeviceList_store = useDeviceStore((state) => state.setDeviceList)
//
useEffect(() => {
const handler = (event, results) => {
setSearching(false)
if (Array.isArray(results) && results.length > 0) {
const ips = results.map((item) => item.from)
setDeviceList(ips.map((ip) => ({ value: ip, label: ip })))
const devices = ips.map((ip) => ({ value: ip, label: ip }))
setDeviceList(devices)
// store
setDeviceList_store(ips.map((ip) => ({ ip, name: ip })))
message.success(`发现${ips.length}台设备`)
} else {
setDeviceList([])
setDeviceList_store([])
message.warning('未发现设备')
}
}
@ -37,21 +47,30 @@ function SiderHeader({ showSystemSettings = true }) {
return () => {
window.electron.ipcRenderer.removeListener(IPC_EVENT.DEVICE_SEARCH_REPLY, handler)
}
}, [])
}, [setDeviceList_store])
// /
useEffect(() => {
const connectHandler = (event, result) => {
if (result.success) {
setConnected(result.ip)
// store
setConnectedDevice({
ip: result.ip,
port: devicePort,
connectedAt: new Date().toISOString()
})
message.success('设备连接成功')
} else {
setConnected(null)
clearConnectedDevice()
message.error('设备连接失败: ' + (result.error || '未知错误'))
}
}
const disconnectHandler = (event, result) => {
setConnected(null)
// store
clearConnectedDevice()
if (result.success) {
message.success('设备已断开')
} else {
@ -67,7 +86,7 @@ function SiderHeader({ showSystemSettings = true }) {
disconnectHandler
)
}
}, [])
}, [devicePort, setConnectedDevice, clearConnectedDevice])
//
const handleSearchDevice = () => {

32
src/renderer/src/stores/deviceStore.js

@ -0,0 +1,32 @@
import { create } from 'zustand'
const useDeviceStore = create((set) => ({
// 当前连接的设备信息
connectedDevice: null,
// 设备列表
deviceList: [],
// 连接状态
isConnecting: false,
// 设置连接的设备
setConnectedDevice: (device) => set({ connectedDevice: device }),
// 清除连接的设备
clearConnectedDevice: () => set({ connectedDevice: null }),
// 设置设备列表
setDeviceList: (devices) => set({ deviceList: devices }),
// 设置连接状态
setConnecting: (status) => set({ isConnecting: status }),
// 获取当前连接的IP
getConnectedIp: () => {
const state = useDeviceStore.getState()
return state.connectedDevice?.ip || null
}
}))
export default useDeviceStore
Loading…
Cancel
Save