monitor.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. import { APIClient } from '@d8d-appcontainer/api'
  2. import debug from "debug"
  3. import { ipMonitor, type IPMonitorResult } from './ip_monitor.ts';
  4. import { readModbusRTU } from './modbus_rtu.ts';
  5. import { Context } from "hono";
  6. import { SmsController } from "../controllers/sms.ts";
  7. import type {
  8. DeviceAlert, DeviceMonitorData
  9. } from '../../client/share/monitorTypes.ts';
  10. import {
  11. DeviceCategory, DeviceStatus, AlertLevel, AlertStatus, NotifyType,
  12. DeviceProtocolType, MetricType, NetworkStatus, PacketLossStatus
  13. } from '../../client/share/monitorTypes.ts';
  14. import {
  15. EnableStatus, DeleteStatus,
  16. } from '../../client/share/types.ts';
  17. const log = {
  18. app:debug('app:monitor'),
  19. debug:debug('debug:monitor')
  20. };
  21. // 设备监控功能
  22. export const startMonitoring = (apiClient: APIClient) => {
  23. log.app('开始设备监控...');
  24. // 传感器类型轮巡配置
  25. const sensorCycles = [
  26. { type: MetricType.TEMPERATURE, interval: 30000 }, // 温度每30秒
  27. { type: MetricType.HUMIDITY, interval: 30000 }, // 湿度每30秒
  28. { type: 'smoke', interval: 60000 }, // 烟感每分钟
  29. { type: 'water', interval: 60000 } // 水浸每分钟
  30. ];
  31. // 启动每种传感器的轮巡定时器
  32. sensorCycles.forEach((sensor) => {
  33. setInterval(async () => {
  34. try {
  35. // 获取需要监控的设备列表,联合查询设备实例和资产信息
  36. const devices = await apiClient.database
  37. .table('device_instances as di')
  38. .leftJoin('zichan_info as zi', 'di.id', 'zi.id')
  39. .select('di.*', 'zi.asset_name', 'zi.device_status', 'zi.device_category')
  40. .where('di.is_enabled', EnableStatus.ENABLED)
  41. .where('di.is_deleted', DeleteStatus.NOT_DELETED)
  42. .where('zi.is_deleted', DeleteStatus.NOT_DELETED);
  43. if (devices.length === 0) {
  44. log.app('没有找到可监控的设备实例');
  45. return;
  46. }
  47. log.app(`找到 ${devices.length} 个可监控的设备实例`);
  48. // 使用Promise.all并发处理所有设备
  49. await Promise.all(devices.map(async (device) => {
  50. try {
  51. // 更新设备的最后采集时间
  52. await apiClient.database.table('device_instances')
  53. .where('id', device.id)
  54. .update({
  55. last_collect_time: apiClient.database.fn.now(),
  56. updated_at: apiClient.database.fn.now()
  57. });
  58. // 生成当前传感器类型的监控数据
  59. const monitorData = await generateMonitorData(device, sensor.type);
  60. if (!monitorData || monitorData.length === 0) {
  61. log.debug(`设备 ${device.id} 未生成监控数据`);
  62. return;
  63. }
  64. // 并发插入监控数据
  65. await Promise.all(monitorData.map(async (data) => {
  66. try {
  67. const [insertId] = await apiClient.database.table('device_monitor_data').insert({
  68. ...data,
  69. collect_time: apiClient.database.fn.now(),
  70. created_at: apiClient.database.fn.now(),
  71. updated_at: apiClient.database.fn.now()
  72. });
  73. const insertedData = await apiClient.database.table('device_monitor_data')
  74. .select('*')
  75. .where('id', insertId)
  76. .first();
  77. const updateData:{network_status?: NetworkStatus, packet_loss?: PacketLossStatus} = {};
  78. if(data.metric_type === MetricType.NETWORK_TRAFFIC) {
  79. updateData.network_status = data.status === DeviceStatus.NORMAL ? NetworkStatus.CONNECTED : NetworkStatus.DISCONNECTED;
  80. }
  81. if(data.metric_type === MetricType.PACKET_LOSS) {
  82. updateData.packet_loss = data.status === DeviceStatus.NORMAL ? PacketLossStatus.NORMAL : PacketLossStatus.HIGH;
  83. }
  84. // 根据监控数据指标更新设备状态
  85. await apiClient.database.table('zichan_info')
  86. .where('id', device.id)
  87. .update({
  88. device_status: insertedData.status,
  89. ...updateData
  90. });
  91. // 检查是否触发告警
  92. if (insertedData) {
  93. await checkAndTriggerAlert(apiClient, insertedData);
  94. }
  95. } catch (error) {
  96. log.app(`监控数据 ${data.metric_type} 插入失败:`, error);
  97. }
  98. }));
  99. } catch (deviceError) {
  100. log.app(`设备 ${device.id} 处理失败:`, deviceError);
  101. }
  102. }));
  103. } catch (error) {
  104. log.app('监控数据收集失败:', error);
  105. }
  106. }, sensor.interval);
  107. });
  108. };
  109. // 生成指定类型的监控数据
  110. export const generateMonitorData = async (device: {
  111. id: number;
  112. device_category?: DeviceCategory;
  113. asset_name?: string;
  114. device_status?: DeviceStatus;
  115. address?: string;
  116. protocol?: DeviceProtocolType;
  117. }, metricType: string) => {
  118. const data = [];
  119. const deviceCategory = device.device_category;
  120. const protocol = device.protocol;
  121. // 根据传感器类型生成特定数据
  122. switch (metricType) {
  123. case MetricType.TEMPERATURE:
  124. case MetricType.HUMIDITY:
  125. // 温湿度传感器数据
  126. if (protocol === DeviceProtocolType.MODBUS && device.address) {
  127. // 实际从MODBUS设备读取数据
  128. const result = await readModbusSensorData(device.address, metricType);
  129. if (result) {
  130. data.push({
  131. device_id: device.id,
  132. metric_type: metricType,
  133. metric_value: result.value,
  134. unit: result.unit,
  135. status: DeviceStatus.NORMAL
  136. });
  137. }
  138. } else {
  139. // 模拟数据
  140. data.push({
  141. device_id: device.id,
  142. metric_type: metricType,
  143. metric_value: metricType === MetricType.TEMPERATURE
  144. ? 20 + Math.random() * 15
  145. : 40 + Math.random() * 40,
  146. unit: metricType === MetricType.TEMPERATURE ? '°C' : '%',
  147. status: DeviceStatus.NORMAL
  148. });
  149. }
  150. break;
  151. case 'smoke':
  152. // 烟感传感器数据
  153. data.push({
  154. device_id: device.id,
  155. metric_type: 'smoke',
  156. metric_value: Math.random() > 0.95 ? 1 : 0, // 5%概率触发
  157. unit: '',
  158. status: Math.random() > 0.95 ? DeviceStatus.FAULT : DeviceStatus.NORMAL
  159. });
  160. break;
  161. case 'water':
  162. // 水浸传感器数据
  163. data.push({
  164. device_id: device.id,
  165. metric_type: 'water',
  166. metric_value: Math.random() > 0.9 ? 1 : 0, // 10%概率触发
  167. unit: '',
  168. status: Math.random() > 0.9 ? DeviceStatus.FAULT : DeviceStatus.NORMAL
  169. });
  170. break;
  171. }
  172. // // 根据设备类型生成不同的监控数据
  173. // if (deviceCategory === DeviceCategory.SERVER) {
  174. // // CPU使用率
  175. // data.push({
  176. // device_id: device.id,
  177. // metric_type: MetricType.CPU_USAGE,
  178. // metric_value: Math.floor(Math.random() * 100),
  179. // unit: '%',
  180. // status: DeviceStatus.NORMAL
  181. // });
  182. // // 内存使用率
  183. // data.push({
  184. // device_id: device.id,
  185. // metric_type: MetricType.MEMORY_USAGE,
  186. // metric_value: Math.floor(Math.random() * 100),
  187. // unit: '%',
  188. // status: DeviceStatus.NORMAL
  189. // });
  190. // // 磁盘使用率
  191. // data.push({
  192. // device_id: device.id,
  193. // metric_type: MetricType.DISK_USAGE,
  194. // metric_value: Math.floor(Math.random() * 90),
  195. // unit: '%',
  196. // status: DeviceStatus.NORMAL
  197. // });
  198. // }
  199. // // 温度 - 所有设备类型
  200. // data.push({
  201. // device_id: device.id,
  202. // metric_type: MetricType.TEMPERATURE,
  203. // metric_value: 20 + Math.random() * 15,
  204. // unit: '°C',
  205. // status: DeviceStatus.NORMAL
  206. // });
  207. // // 湿度 - 所有设备类型
  208. // data.push({
  209. // device_id: device.id,
  210. // metric_type: MetricType.HUMIDITY,
  211. // metric_value: 40 + Math.random() * 40,
  212. // unit: '%',
  213. // status: DeviceStatus.NORMAL
  214. // });
  215. // // 网络流量 - 所有设备类型
  216. // data.push({
  217. // device_id: device.id,
  218. // metric_type: MetricType.NETWORK_TRAFFIC,
  219. // metric_value: Math.random() * 1000,
  220. // unit: 'MB/s',
  221. // status: DeviceStatus.NORMAL
  222. // });
  223. // IP地址连通性检测 - 根据不同协议生成不同的监控数据
  224. if (protocol === DeviceProtocolType.TCP && device.address) {
  225. // 使用真实的 IP 监控数据
  226. const ipResult = await new Promise<IPMonitorResult>((resolve) => {
  227. ipMonitor.startMonitor(device.address!, (result) => {
  228. resolve(result);
  229. });
  230. });
  231. // 根据协议类型添加特定的监控数据
  232. // switch (protocol) {
  233. // case DeviceProtocolType.SNMP: {
  234. // // SNMP协议设备 - 添加SNMP特有的监控指标
  235. // const snmpResponseTime = Math.floor(10 + Math.random() * 90);
  236. // data.push({
  237. // device_id: device.id,
  238. // metric_type: MetricType.SNMP_RESPONSE_TIME,
  239. // metric_value: snmpResponseTime,
  240. // unit: 'ms',
  241. // status: snmpResponseTime > 80 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL
  242. // });
  243. // // SNMP错误计数
  244. // const snmpErrors = Math.floor(Math.random() * 5);
  245. // data.push({
  246. // device_id: device.id,
  247. // metric_type: MetricType.SNMP_ERRORS,
  248. // metric_value: snmpErrors,
  249. // unit: '',
  250. // status: snmpErrors > 2 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL
  251. // });
  252. // break;
  253. // }
  254. // case DeviceProtocolType.HTTP: {
  255. // // HTTP协议设备 - 添加HTTP特有的监控指标
  256. // // HTTP响应时间
  257. // const httpResponseTime = Math.floor(20 + Math.random() * 200);
  258. // data.push({
  259. // device_id: device.id,
  260. // metric_type: MetricType.HTTP_RESPONSE_TIME,
  261. // metric_value: httpResponseTime,
  262. // unit: 'ms',
  263. // status: httpResponseTime > 180 ? DeviceStatus.FAULT :
  264. // httpResponseTime > 100 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL
  265. // });
  266. // // HTTP状态码
  267. // const httpStatus = Math.random() > 0.9 ? 500 :
  268. // Math.random() > 0.95 ? 404 : 200;
  269. // data.push({
  270. // device_id: device.id,
  271. // metric_type: MetricType.HTTP_STATUS,
  272. // metric_value: httpStatus,
  273. // unit: '',
  274. // status: httpStatus !== 200 ? DeviceStatus.FAULT : DeviceStatus.NORMAL
  275. // });
  276. // break;
  277. // }
  278. // case DeviceProtocolType.TCP: {
  279. // // TCP协议设备
  280. // // TCP连接时间
  281. // const tcpConnTime = Math.floor(5 + Math.random() * 45);
  282. // data.push({
  283. // device_id: device.id,
  284. // metric_type: MetricType.TCP_CONNECTION_TIME,
  285. // metric_value: tcpConnTime,
  286. // unit: 'ms',
  287. // status: tcpConnTime > 30 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL
  288. // });
  289. // break;
  290. // }
  291. // }
  292. // 添加 IP 监控数据
  293. if (ipResult.success) {
  294. data.push({
  295. device_id: device.id,
  296. metric_type: MetricType.PING_TIME,
  297. metric_value: ipResult.responseTime || 0,
  298. unit: 'ms',
  299. status: ipResult.responseTime && ipResult.responseTime > 150 ? DeviceStatus.FAULT :
  300. ipResult.responseTime && ipResult.responseTime > 100 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL
  301. });
  302. data.push({
  303. device_id: device.id,
  304. metric_type: MetricType.PACKET_LOSS,
  305. metric_value: ipResult.packetLoss || 0,
  306. unit: '%',
  307. status: ipResult.packetLoss && ipResult.packetLoss > 5 ? DeviceStatus.FAULT :
  308. ipResult.packetLoss && ipResult.packetLoss > 2 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL
  309. });
  310. data.push({
  311. device_id: device.id,
  312. metric_type: MetricType.CONNECTION_STATUS,
  313. metric_value: ipResult.success ? NetworkStatus.CONNECTED : NetworkStatus.DISCONNECTED,
  314. unit: '',
  315. status: ipResult.success ? DeviceStatus.NORMAL : DeviceStatus.FAULT
  316. });
  317. } else {
  318. // IP 监控失败
  319. data.push({
  320. device_id: device.id,
  321. metric_type: MetricType.CONNECTION_STATUS,
  322. metric_value: NetworkStatus.DISCONNECTED,
  323. unit: '',
  324. status: DeviceStatus.FAULT
  325. });
  326. }
  327. }
  328. return data;
  329. };
  330. // 检查是否触发告警
  331. export const checkAndTriggerAlert = async (apiClient: APIClient, monitorData: DeviceMonitorData) => {
  332. try {
  333. // 获取设备告警规则
  334. const rules = await apiClient.database.table('device_alert_rules')
  335. .select('*')
  336. .where('device_id', monitorData.device_id)
  337. .where('metric_type', monitorData.metric_type)
  338. .where('is_enabled', EnableStatus.ENABLED)
  339. .where('is_deleted', DeleteStatus.NOT_DELETED);
  340. if (!rules || rules.length === 0) {
  341. log.app(`未找到设备ID: ${monitorData.device_id} 的告警规则`);
  342. return;
  343. }
  344. // 获取设备信息 - 联合查询设备实例和资产信息
  345. const deviceInfo = await apiClient.database
  346. .table('device_instances as di')
  347. .leftJoin('zichan_info as zi', 'di.id', 'zi.id')
  348. .select('di.*', 'zi.asset_name', 'zi.device_category')
  349. .where('di.id', monitorData.device_id)
  350. .first();
  351. if (!deviceInfo) {
  352. log.app(`未找到设备ID: ${monitorData.device_id} 的信息`);
  353. return;
  354. }
  355. // 检查每条规则
  356. for (const rule of rules) {
  357. let isTriggered = false;
  358. // 确保 最小值 和 最大值 是数字
  359. if (rule.min_value !== null && typeof rule.min_value !== 'number') {
  360. rule.min_value = Number(rule.min_value);
  361. }
  362. if (rule.max_value !== null && typeof rule.max_value !== 'number') {
  363. rule.max_value = Number(rule.max_value);
  364. }
  365. // 检查最小值
  366. if (rule.min_value !== null && monitorData.metric_value < rule.min_value) {
  367. isTriggered = true;
  368. }
  369. // 检查最大值
  370. if (rule.max_value !== null && monitorData.metric_value > rule.max_value) {
  371. isTriggered = true;
  372. }
  373. if (isTriggered) {
  374. // 替换告警消息模板中的变量
  375. let alertMessage = rule.alert_message || `设备${deviceInfo.asset_name || deviceInfo.id}的${monitorData.metric_type}值异常: ${monitorData.metric_value}${monitorData.unit || ''}`;
  376. // 替换模板变量
  377. alertMessage = alertMessage
  378. .replace(/\{\{device_name\}\}/g, deviceInfo.asset_name || `设备${deviceInfo.id}`)
  379. .replace(/\{\{metric_value\}\}/g, monitorData.metric_value.toString())
  380. .replace(/\{\{unit\}\}/g, monitorData.unit || '');
  381. // 创建告警记录
  382. const [alertId] = await apiClient.database.table('device_alerts').insert({
  383. device_id: monitorData.device_id,
  384. device_name: deviceInfo.asset_name || `设备${deviceInfo.id}`,
  385. metric_type: monitorData.metric_type,
  386. metric_value: monitorData.metric_value,
  387. alert_level: rule.alert_level,
  388. alert_message: alertMessage,
  389. status: AlertStatus.PENDING,
  390. created_at: apiClient.database.fn.now(),
  391. updated_at: apiClient.database.fn.now()
  392. });
  393. const alert = await apiClient.database.table('device_alerts')
  394. .select('*')
  395. .where('id', alertId)
  396. .first();
  397. if (alert) {
  398. log.app(`触发告警: ${alertMessage}`);
  399. // 发送告警通知
  400. await sendAlertNotification(apiClient, alert);
  401. }
  402. }
  403. }
  404. } catch (error) {
  405. log.app('检查告警失败:', error);
  406. }
  407. };
  408. // 发送告警通知
  409. export const sendAlertNotification = async (apiClient: APIClient, alert: DeviceAlert) => {
  410. try {
  411. // 查询告警通知配置
  412. const notifyConfigs = await apiClient.database.table('alert_notify_configs')
  413. .select('*')
  414. .where('device_id', alert.device_id)
  415. .where('alert_level', alert.alert_level)
  416. .where('is_enabled', EnableStatus.ENABLED)
  417. .where('is_deleted', DeleteStatus.NOT_DELETED);
  418. if (!notifyConfigs || notifyConfigs.length === 0) {
  419. log.app(`设备 ${alert.device_id} 没有配置告警级别 ${alert.alert_level} 的通知`);
  420. return;
  421. }
  422. // 对每个通知配置进行处理
  423. for (const config of notifyConfigs) {
  424. log.debug('通知配置 %O',config)
  425. // 解析通知用户列表
  426. const notifyUsers = config.notify_users || [];
  427. if (notifyUsers.length === 0) {
  428. log.app(`设备 ${alert.device_id} 的通知配置 ${config.id} 没有指定通知用户`);
  429. continue;
  430. }
  431. // 查询用户信息
  432. const users = await apiClient.database.table('users')
  433. .select('*')
  434. .whereIn('id', notifyUsers);
  435. if (!users || users.length === 0) {
  436. log.app(`找不到通知配置 ${config.id} 指定的用户`);
  437. continue;
  438. }
  439. // 替换通知模板变量
  440. let notifyContent = config.notify_template || `告警: ${alert.alert_message}`;
  441. notifyContent = notifyContent
  442. .replace(/\{\{alert_message\}\}/g, alert.alert_message)
  443. .replace(/\{\{alert_level\}\}/g, AlertLevel[alert.alert_level] || '未知');
  444. // 根据通知类型发送通知
  445. switch (config.notify_type) {
  446. case NotifyType.SMS:
  447. // 实现短信通知
  448. for (const user of users) {
  449. if (user.phone) {
  450. try {
  451. const smsContent = `【告警通知】
  452. 设备ID: ${alert.device_id}
  453. 告警级别: ${AlertLevel[alert.alert_level] || '未知'}
  454. 告警时间: ${new Date(alert.created_at).toLocaleString()}
  455. 告警内容: ${alert.alert_message}`;
  456. const mockCtx = {
  457. req: {
  458. json: async () => ({
  459. phone: user.phone,
  460. content: smsContent
  461. })
  462. }
  463. } as Context;
  464. await SmsController.sendSms(mockCtx);
  465. log.app(`成功发送短信通知给用户 ${user.username}(${user.phone})`);
  466. } catch (error) {
  467. log.app(`发送短信通知给用户 ${user.username}(${user.phone}) 失败:`, error);
  468. }
  469. } else {
  470. log.app(`用户 ${user.username} 没有配置手机号,无法发送短信通知`);
  471. }
  472. }
  473. break;
  474. case NotifyType.EMAIL:
  475. // 实现邮件通知
  476. for (const user of users) {
  477. if (user.email) {
  478. log.app(`向用户 ${user.username}(${user.email}) 发送邮件通知: ${notifyContent}`);
  479. // 实际环境中调用邮件发送API
  480. } else {
  481. log.app(`用户 ${user.username} 没有配置邮箱,无法发送邮件通知`);
  482. }
  483. }
  484. break;
  485. case NotifyType.WECHAT:
  486. // 实现微信通知
  487. for (const user of users) {
  488. log.app(`向用户 ${user.username} 发送微信通知: ${notifyContent}`);
  489. // 实际环境中调用微信发送API
  490. }
  491. break;
  492. default:
  493. log.app(`不支持的通知类型: ${config.notify_type}`);
  494. break;
  495. }
  496. }
  497. } catch (error) {
  498. log.app('发送告警通知失败:', error);
  499. }
  500. };
  501. // 从MODBUS设备读取传感器数据
  502. async function readModbusSensorData(
  503. address: string,
  504. metricType: string
  505. ): Promise<{ value: number; unit: string } | null> {
  506. try {
  507. // 根据传感器类型设置不同的寄存器地址
  508. let registerAddress = 0;
  509. switch (metricType) {
  510. case MetricType.TEMPERATURE:
  511. registerAddress = 0x1000; // 温度寄存器地址
  512. break;
  513. case MetricType.HUMIDITY:
  514. registerAddress = 0x1002; // 湿度寄存器地址
  515. break;
  516. case 'smoke':
  517. registerAddress = 0x1004; // 烟感寄存器地址
  518. break;
  519. case 'water':
  520. registerAddress = 0x1006; // 水浸寄存器地址
  521. break;
  522. }
  523. // 调用modbus_rtu.ts中的方法读取数据
  524. const result = await readModbusRTU(address, registerAddress, 2);
  525. // 解析返回的数据
  526. if (result && result.length >= 2) {
  527. const value = (result[0] << 8) | result[1]; // 组合高低字节
  528. return {
  529. value: metricType === 'smoke' || metricType === 'water'
  530. ? value > 0 ? 1 : 0 // 烟感/水浸为开关量
  531. : value / 10, // 温湿度为模拟量,除以10得到实际值
  532. unit: metricType === MetricType.TEMPERATURE ? '°C' :
  533. metricType === MetricType.HUMIDITY ? '%' : ''
  534. };
  535. }
  536. return null;
  537. } catch (error) {
  538. log.app(`读取MODBUS传感器数据失败: ${error}`);
  539. return null;
  540. }
  541. }
  542. // 设备监控功能
  543. /**
  544. * 解析温湿度传感器十六进制数据
  545. * @param hexData 十六进制数据字符串,格式如"[02 04 04 01 02 02 65 A9 F3]"
  546. * @returns { temperature: number, humidity: number } 解析后的温湿度对象
  547. * @throws 如果数据格式无效会抛出错误
  548. */
  549. export function parseTemperatureHumidity(hexData: string): {
  550. temperature: { value: number; unit: string };
  551. humidity: { value: number; unit: string };
  552. error?: string;
  553. } {
  554. try {
  555. // 验证数据格式
  556. if (!/^\[\s*(?:[0-9A-Fa-f]{2}\s*)+\]$/.test(hexData)) {
  557. return {
  558. temperature: { value: 0, unit: '°C' },
  559. humidity: { value: 0, unit: '%' },
  560. error: '数据格式错误: 必须以方括号开头和结尾'
  561. };
  562. }
  563. // 提取十六进制字节数组
  564. const bytes = hexData
  565. .replace(/[\[\]\s]/g, '')
  566. .match(/.{1,2}/g)
  567. ?.map(byte => parseInt(byte, 16)) || [];
  568. if (bytes.length < 7) {
  569. return {
  570. temperature: { value: 0, unit: '°C' },
  571. humidity: { value: 0, unit: '%' },
  572. error: '数据长度不足: 至少需要7个十六进制字节'
  573. };
  574. }
  575. // 解析温度值 (01位)
  576. const tempInt = bytes[3]; // 整数部分
  577. const tempFrac = bytes[4]; // 小数部分
  578. const temperature = parseFloat(`${tempInt}.${tempFrac}`);
  579. // 解析湿度值 (02位)
  580. const humidityInt = bytes[5]; // 整数部分
  581. const humidityFrac = bytes[6]; // 小数部分
  582. const humidity = parseFloat(`${humidityInt}.${humidityFrac}`);
  583. return {
  584. temperature: { value: temperature, unit: '°C' },
  585. humidity: { value: humidity, unit: '%' }
  586. };
  587. } catch (error) {
  588. return {
  589. temperature: { value: 0, unit: '°C' },
  590. humidity: { value: 0, unit: '%' },
  591. error: '数据解析失败: ' + (error instanceof Error ? error.message : String(error))
  592. };
  593. }
  594. }