pages_greenhouse_protocol.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import React, { useState } from 'react';
  2. import {
  3. Card,
  4. Form,
  5. Input,
  6. Button,
  7. Table,
  8. Tag,
  9. Space,
  10. Statistic,
  11. InputNumber,
  12. message,
  13. Modal,
  14. Checkbox
  15. } from 'antd';
  16. import {
  17. ThunderboltOutlined,
  18. PlusOutlined,
  19. DeleteOutlined,
  20. EditOutlined
  21. } from '@ant-design/icons';
  22. interface DeviceConfig {
  23. id: string;
  24. name: string;
  25. deviceNo: string;
  26. ipAddress: string;
  27. port: number;
  28. gatewayBaudRate: number;
  29. address: number;
  30. baudRate: number;
  31. dataBits: number;
  32. stopBits: number;
  33. parity: 'none' | 'even' | 'odd';
  34. timeout: number;
  35. unitId: number;
  36. verificationCode?: string;
  37. // 报警阈值配置
  38. tempThreshold: {
  39. min: number;
  40. max: number;
  41. };
  42. humidityThreshold: {
  43. min: number;
  44. max: number;
  45. };
  46. alertMethods: {
  47. push: boolean;
  48. sms: boolean;
  49. };
  50. }
  51. interface SensorData {
  52. temperature: number;
  53. humidity: number;
  54. timestamp: string;
  55. }
  56. // 临时模拟ModbusRTU功能
  57. const ModbusRTU = {
  58. connect: async (device: Omit<DeviceConfig, 'id'>): Promise<boolean> => {
  59. console.log('模拟连接设备:', device);
  60. return true;
  61. },
  62. readHoldingRegisters: async (addr: number, count: number): Promise<number[]> => {
  63. console.log('模拟读取寄存器:', addr, count);
  64. return [255, 652]; // 模拟温度25.5°C和湿度65.2%
  65. }
  66. };
  67. const GreenhouseProtocolPage: React.FC = () => {
  68. const [editingDevice, setEditingDevice] = useState<DeviceConfig | null>(null);
  69. const [modalVisible, setModalVisible] = useState(false);
  70. const [form] = Form.useForm();
  71. const [devices, setDevices] = useState<DeviceConfig[]>([]);
  72. const [currentData, setCurrentData] = useState<SensorData | null>(null);
  73. const [loading, setLoading] = useState(false);
  74. const [connected, setConnected] = useState(false);
  75. const generateVerificationCode = () => {
  76. const code = Math.floor(Math.random() * 65536).toString(16).toUpperCase();
  77. return code.length === 1 ? `0${code}` : code.padStart(2, '0');
  78. };
  79. const handleAddDevice = () => {
  80. const values = form.getFieldsValue();
  81. setDevices([...devices, {
  82. id: `device-${Date.now()}`,
  83. deviceNo: values.deviceNo,
  84. verificationCode: generateVerificationCode(),
  85. tempThreshold: {
  86. min: 10,
  87. max: 30
  88. },
  89. humidityThreshold: {
  90. min: 30,
  91. max: 80
  92. },
  93. alertMethods: {
  94. push: true,
  95. sms: false
  96. },
  97. ...values
  98. }]);
  99. form.resetFields();
  100. };
  101. const handleSendCommand = (device: DeviceConfig) => {
  102. if (!device.verificationCode) {
  103. message.error('请先生成校验码');
  104. return;
  105. }
  106. const deviceNoHex = parseInt(device.deviceNo).toString(16).padStart(2, '0').toUpperCase();
  107. const command = `[${deviceNoHex} 04 00 00 00 02 ${device.verificationCode}]`;
  108. // 模拟返回数据 (温度18-25℃, 湿度40-70%)
  109. const temp = (18 + Math.random() * 7).toFixed(1);
  110. const humidity = (40 + Math.random() * 30).toFixed(1);
  111. const tempInt = parseInt(temp);
  112. const tempDec = parseInt((temp.split('.')[1] || '0'));
  113. const humInt = parseInt(humidity);
  114. const humDec = parseInt((humidity.split('.')[1] || '0'));
  115. const response = `[${deviceNoHex} 04 04 ${tempInt.toString(16).padStart(2,'0')} ${tempDec.toString(16).padStart(2,'0')} ${humInt.toString(16).padStart(2,'0')} ${humDec.toString(16).padStart(2,'0')} 9B 1E]`;
  116. console.log('发送指令:', command);
  117. message.success(<>
  118. <div>指令已发送:</div>
  119. <div style={{fontFamily: 'monospace', marginBottom: 8}}>{command}</div>
  120. <div>返回数据:</div>
  121. <div style={{fontFamily: 'monospace', marginBottom: 8}}>{response}</div>
  122. <div>解析结果: 温度 {temp}℃, 湿度 {humidity}%</div>
  123. </>);
  124. };
  125. const handleConnect = async (device: DeviceConfig) => {
  126. setLoading(true);
  127. try {
  128. await ModbusRTU.connect(device);
  129. setConnected(true);
  130. } catch (err) {
  131. console.error('连接失败:', err);
  132. } finally {
  133. setLoading(false);
  134. }
  135. };
  136. const handleReadData = async () => {
  137. if (!connected) return;
  138. setLoading(true);
  139. try {
  140. const data = await ModbusRTU.readHoldingRegisters(0, 2);
  141. setCurrentData({
  142. temperature: data[0] / 10,
  143. humidity: data[1] / 10,
  144. timestamp: new Date().toISOString()
  145. });
  146. } catch (err) {
  147. console.error('读取数据失败:', err);
  148. } finally {
  149. setLoading(false);
  150. }
  151. };
  152. const columns = [
  153. {
  154. title: '报警状态',
  155. key: 'alertStatus',
  156. render: (_: unknown, record: DeviceConfig) => {
  157. const tempAlert = currentData && (
  158. currentData.temperature < record.tempThreshold.min ||
  159. currentData.temperature > record.tempThreshold.max
  160. );
  161. const humidityAlert = currentData && (
  162. currentData.humidity < record.humidityThreshold.min ||
  163. currentData.humidity > record.humidityThreshold.max
  164. );
  165. return (
  166. <Space>
  167. {tempAlert && <Tag color="error">温度异常</Tag>}
  168. {humidityAlert && <Tag color="error">湿度异常</Tag>}
  169. {!tempAlert && !humidityAlert && <Tag color="success">正常</Tag>}
  170. </Space>
  171. );
  172. },
  173. },
  174. {
  175. title: '设备ID',
  176. dataIndex: 'id',
  177. key: 'id',
  178. },
  179. {
  180. title: '设备编号',
  181. dataIndex: 'deviceNo',
  182. key: 'deviceNo',
  183. },
  184. {
  185. title: '设备名称',
  186. dataIndex: 'name',
  187. key: 'name',
  188. },
  189. {
  190. title: 'Modbus地址',
  191. dataIndex: 'address',
  192. key: 'address',
  193. },
  194. {
  195. title: '状态',
  196. key: 'status',
  197. render: () => (
  198. <Tag color={connected ? 'success' : 'error'}>
  199. {connected ? '已连接' : '未连接'}
  200. </Tag>
  201. ),
  202. },
  203. {
  204. title: '操作',
  205. key: 'action',
  206. render: (_: unknown, record: DeviceConfig) => (
  207. <Space size="middle">
  208. <Button
  209. type="link"
  210. icon={<EditOutlined />}
  211. onClick={() => {
  212. setEditingDevice(record);
  213. setModalVisible(true);
  214. }}
  215. >
  216. 编辑
  217. </Button>
  218. <Button
  219. type="link"
  220. danger
  221. icon={<DeleteOutlined />}
  222. onClick={() => {
  223. Modal.confirm({
  224. title: '确认删除设备配置?',
  225. content: `确定要删除设备 ${record.name} 吗?`,
  226. onOk: () => {
  227. setDevices((prev: DeviceConfig[]) => prev.filter(d => d.id !== record.id));
  228. message.success('设备配置已删除');
  229. },
  230. });
  231. }}
  232. >
  233. 删除
  234. </Button>
  235. <Button
  236. type="primary"
  237. onClick={() => handleConnect(record)}
  238. loading={loading}
  239. >
  240. 连接
  241. </Button>
  242. <Button
  243. icon={<ThunderboltOutlined />}
  244. onClick={() => handleReadData()}
  245. disabled={!connected}
  246. >
  247. 读取数据
  248. </Button>
  249. </Space>
  250. ),
  251. },
  252. ];
  253. return (
  254. <Card title="温室设备通信协议配置">
  255. <Form form={form} layout="inline">
  256. <Form.Item name="name" label="设备名称" rules={[{ required: true }]}>
  257. <Input placeholder="例如: 温室1号" />
  258. </Form.Item>
  259. <Form.Item name="deviceNo" label="设备编号" rules={[{ required: true, message: '请输入设备编号' }]}>
  260. <Input placeholder="例如: GH001" />
  261. </Form.Item>
  262. <Form.Item name="address" label="Modbus地址" rules={[{ required: true }]}>
  263. <InputNumber min={1} max={247} />
  264. </Form.Item>
  265. <Form.Item>
  266. <Button type="primary" onClick={handleAddDevice}>
  267. 添加设备
  268. </Button>
  269. </Form.Item>
  270. </Form>
  271. <Modal
  272. title="编辑设备配置"
  273. visible={modalVisible}
  274. onCancel={() => setModalVisible(false)}
  275. footer={null}
  276. >
  277. <Form
  278. form={form}
  279. initialValues={editingDevice || {
  280. tempThreshold: { min: 10, max: 30 },
  281. humidityThreshold: { min: 30, max: 80 },
  282. alertMethods: { push: true, sms: false }
  283. }}
  284. layout="vertical"
  285. >
  286. <Form.Item name="deviceNo" label="设备编号" rules={[{ required: true }]}>
  287. <Input />
  288. </Form.Item>
  289. <Form.Item name="ipAddress" label="IP地址" rules={[{ required: true }]}>
  290. <Input placeholder="例如: 192.168.1.1" />
  291. </Form.Item>
  292. <Form.Item name="port" label="端口号" rules={[{ required: true }]}>
  293. <InputNumber min={1} max={65535} />
  294. </Form.Item>
  295. <Form.Item name="gatewayBaudRate" label="网关波特率" rules={[{ required: true }]}>
  296. <InputNumber min={1200} max={115200} />
  297. </Form.Item>
  298. <Form.Item>
  299. <Button
  300. type="primary"
  301. onClick={() => {
  302. // 获取校验码逻辑
  303. message.info('校验码已发送');
  304. }}
  305. >
  306. 获取校验码
  307. </Button>
  308. <Button
  309. icon={<ThunderboltOutlined />}
  310. onClick={() => {
  311. if (editingDevice) {
  312. handleSendCommand(editingDevice);
  313. }
  314. }}
  315. style={{ marginLeft: 16 }}
  316. >
  317. 发送指令
  318. </Button>
  319. </Form.Item>
  320. <Form.Item name="verificationCode" label="校验码">
  321. <Input disabled />
  322. </Form.Item>
  323. <Card title="报警阈值设置" style={{ marginBottom: 16 }}>
  324. <Form.Item label="温度阈值(°C)" style={{ marginBottom: 8 }}>
  325. <Space>
  326. <Form.Item name={['tempThreshold', 'min']} noStyle>
  327. <InputNumber min={-20} max={100} placeholder="最低" />
  328. </Form.Item>
  329. <span>~</span>
  330. <Form.Item name={['tempThreshold', 'max']} noStyle>
  331. <InputNumber min={-20} max={100} placeholder="最高" />
  332. </Form.Item>
  333. </Space>
  334. </Form.Item>
  335. <Form.Item label="湿度阈值(%)" style={{ marginBottom: 8 }}>
  336. <Space>
  337. <Form.Item name={['humidityThreshold', 'min']} noStyle>
  338. <InputNumber min={0} max={100} placeholder="最低" />
  339. </Form.Item>
  340. <span>~</span>
  341. <Form.Item name={['humidityThreshold', 'max']} noStyle>
  342. <InputNumber min={0} max={100} placeholder="最高" />
  343. </Form.Item>
  344. </Space>
  345. </Form.Item>
  346. <Form.Item label="报警方式">
  347. <Space>
  348. <Form.Item name={['alertMethods', 'push']} valuePropName="checked" noStyle>
  349. <Checkbox>消息推送</Checkbox>
  350. </Form.Item>
  351. <Form.Item name={['alertMethods', 'sms']} valuePropName="checked" noStyle>
  352. <Checkbox>短信通知</Checkbox>
  353. </Form.Item>
  354. </Space>
  355. </Form.Item>
  356. </Card>
  357. </Form>
  358. </Modal>
  359. <Table
  360. columns={columns}
  361. dataSource={devices}
  362. rowKey="id"
  363. style={{ marginTop: 16 }}
  364. />
  365. {currentData && (
  366. <Card title="实时数据" style={{ marginTop: 16 }}>
  367. <Space size="large">
  368. <Statistic
  369. title="温度"
  370. value={currentData.temperature}
  371. suffix="°C"
  372. precision={1}
  373. />
  374. <Statistic
  375. title="湿度"
  376. value={currentData.humidity}
  377. suffix="%"
  378. precision={1}
  379. />
  380. <div>最后更新: {new Date(currentData.timestamp).toLocaleString()}</div>
  381. </Space>
  382. </Card>
  383. )}
  384. </Card>
  385. );
  386. };
  387. export { GreenhouseProtocolPage };