app.py 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399
  1. from flask import Flask, jsonify, request, abort
  2. from flask_cors import CORS
  3. from flask_socketio import SocketIO, emit
  4. import threading
  5. import time
  6. import json
  7. import os
  8. import logging
  9. import uuid
  10. # 导入配置
  11. from config import (
  12. MAX_BUFFER_SIZE,
  13. FLASK_SECRET_KEY,
  14. FLASK_DEBUG,
  15. FLASK_HOST,
  16. FLASK_PORT,
  17. LOG_LEVEL,
  18. LOG_FORMAT,
  19. LOG_FILE,
  20. SOCKETIO_ASYNC_MODE,
  21. SOCKETIO_ALLOWED_ORIGINS,
  22. DEFAULT_FORWARD_SERIAL_TO_MQTT,
  23. DEFAULT_FORWARD_MQTT_TO_SERIAL,
  24. DEFAULT_MQTT_PUBLISH_TOPIC,
  25. SOCKETIO_NAMESPACE_DATA,
  26. SOCKETIO_NAMESPACE_STATUS,
  27. SOCKETIO_NAMESPACE_CONTROL,
  28. ERROR_CODES,
  29. ERROR_MESSAGES
  30. )
  31. # 配置日志
  32. logging_config = {
  33. 'level': getattr(logging, LOG_LEVEL),
  34. 'format': LOG_FORMAT
  35. }
  36. if LOG_FILE:
  37. logging_config['filename'] = LOG_FILE
  38. logging.basicConfig(**logging_config)
  39. logger = logging.getLogger('serial_mqtt_gateway')
  40. from modules.serial_port import SerialPort
  41. from modules.mqtt_client import MQTTClient
  42. from modules.network_config import network_manager
  43. from modules.modbus_rtu import ModbusRTUClient, ANTENNA_ADDRESSES, AddressConfigProtocol, build_broadcast_query, build_confirm_address, build_assign_address
  44. app = Flask(__name__)
  45. app.config['SECRET_KEY'] = FLASK_SECRET_KEY
  46. # 配置CORS以允许nginx代理的前端访问
  47. CORS(app, resources={r"/api/*": {"origins": "*"}, r"/socket.io/*": {"origins": "*"}})
  48. # 初始化SocketIO
  49. socketio = SocketIO(
  50. app,
  51. cors_allowed_origins=SOCKETIO_ALLOWED_ORIGINS,
  52. async_mode=SOCKETIO_ASYNC_MODE,
  53. manage_session=False, # 禁用会话管理以提高性能
  54. ping_timeout=30, # 心跳超时时间
  55. ping_interval=25, # 心跳间隔
  56. logger=FLASK_DEBUG, # 根据Flask调试模式决定是否记录SocketIO日志
  57. engineio_logger=FLASK_DEBUG
  58. )
  59. # 初始化串口和MQTT客户端
  60. serial_client = SerialPort()
  61. mqtt_client = MQTTClient()
  62. modbus_client = ModbusRTUClient(serial_client)
  63. address_config = AddressConfigProtocol(serial_client)
  64. # 转发标志
  65. forward_serial_to_mqtt = DEFAULT_FORWARD_SERIAL_TO_MQTT
  66. forward_mqtt_to_serial = DEFAULT_FORWARD_MQTT_TO_SERIAL
  67. mqtt_publish_topic = DEFAULT_MQTT_PUBLISH_TOPIC
  68. # 数据存储缓冲区
  69. serial_data_buffer = []
  70. mqtt_data_buffer = []
  71. # 状态标志
  72. serial_status = False
  73. mqtt_status = False
  74. # 客户端连接管理
  75. connected_clients = {
  76. 'data': set(),
  77. 'status': set(),
  78. 'control': set()
  79. }
  80. # 设置回调函数
  81. def serial_data_handler(data):
  82. """处理串口接收的数据"""
  83. try:
  84. # 添加到缓冲区
  85. timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
  86. serial_data_buffer.append({
  87. 'timestamp': timestamp,
  88. 'data': data
  89. })
  90. # 保持缓冲区大小
  91. if len(serial_data_buffer) > MAX_BUFFER_SIZE:
  92. serial_data_buffer.pop(0)
  93. # 通过WebSocket广播数据
  94. socketio.emit('serial_data', {
  95. 'timestamp': timestamp,
  96. 'data': data
  97. }, namespace=SOCKETIO_NAMESPACE_DATA)
  98. # 如果启用了转发且MQTT已连接,转发数据到MQTT
  99. if forward_serial_to_mqtt and mqtt_client.get_status():
  100. success, msg = mqtt_client.publish(mqtt_publish_topic, data)
  101. if not success:
  102. logger.warning(f"串口数据转发到MQTT失败: {msg}")
  103. except Exception as e:
  104. logger.error(f"处理串口数据时出错: {str(e)}")
  105. def serial_status_handler(status):
  106. """处理串口状态变化"""
  107. try:
  108. global serial_status
  109. serial_status = status
  110. # 通过WebSocket广播状态变化
  111. socketio.emit('serial_status', {
  112. 'connected': status
  113. }, namespace=SOCKETIO_NAMESPACE_STATUS)
  114. logger.info(f"串口状态更新: {'已连接' if status else '已断开'}")
  115. except Exception as e:
  116. logger.error(f"处理串口状态时出错: {str(e)}")
  117. def mqtt_data_handler(data):
  118. """处理MQTT接收的数据"""
  119. try:
  120. # 添加到缓冲区
  121. timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
  122. mqtt_data_buffer.append({
  123. 'timestamp': timestamp,
  124. 'topic': data['topic'],
  125. 'payload': data['payload']
  126. })
  127. # 保持缓冲区大小
  128. if len(mqtt_data_buffer) > MAX_BUFFER_SIZE:
  129. mqtt_data_buffer.pop(0)
  130. # 通过WebSocket广播数据
  131. socketio.emit('mqtt_data', {
  132. 'timestamp': timestamp,
  133. 'topic': data['topic'],
  134. 'payload': data['payload']
  135. }, namespace=SOCKETIO_NAMESPACE_DATA)
  136. # 如果启用了转发且串口已连接,转发数据到串口
  137. if forward_mqtt_to_serial and serial_client.get_status():
  138. success, msg = serial_client.send_data(data['payload'])
  139. if not success:
  140. logger.warning(f"MQTT数据转发到串口失败: {msg}")
  141. except Exception as e:
  142. logger.error(f"处理MQTT数据时出错: {str(e)}")
  143. def mqtt_status_handler(status):
  144. """处理MQTT状态变化"""
  145. try:
  146. global mqtt_status
  147. mqtt_status = status
  148. # 通过WebSocket广播状态变化
  149. socketio.emit('mqtt_status', {
  150. 'connected': status
  151. }, namespace=SOCKETIO_NAMESPACE_STATUS)
  152. logger.info(f"MQTT状态更新: {'已连接' if status else '已断开'}")
  153. except Exception as e:
  154. logger.error(f"处理MQTT状态时出错: {str(e)}")
  155. # WebSocket事件处理
  156. @socketio.on('connect', namespace=SOCKETIO_NAMESPACE_DATA)
  157. def handle_data_connect():
  158. """处理数据命名空间的连接"""
  159. try:
  160. client_id = str(uuid.uuid4())
  161. connected_clients['data'].add(client_id)
  162. logger.info(f'客户端已连接到数据命名空间,当前连接数: {len(connected_clients["data"])}')
  163. # 发送当前的缓冲区数据,限制发送的历史记录数量
  164. max_history = 100 # 限制发送的历史记录数量以提高性能
  165. emit('serial_data_history', {'data': serial_data_buffer[-max_history:]})
  166. emit('mqtt_data_history', {'data': mqtt_data_buffer[-max_history:]})
  167. # 存储客户端ID以便断开连接时使用
  168. socketio.start_background_task(target=lambda: None) # 确保上下文可用
  169. except Exception as e:
  170. logger.error(f"处理数据命名空间连接时出错: {str(e)}")
  171. @socketio.on('connect', namespace=SOCKETIO_NAMESPACE_STATUS)
  172. def handle_status_connect():
  173. """处理状态命名空间的连接"""
  174. try:
  175. client_id = str(uuid.uuid4())
  176. connected_clients['status'].add(client_id)
  177. logger.info(f'客户端已连接到状态命名空间,当前连接数: {len(connected_clients["status"])}')
  178. # 发送当前状态
  179. emit('serial_status', {'connected': serial_status})
  180. emit('mqtt_status', {'connected': mqtt_status})
  181. emit('forward_status', {
  182. 'serial_to_mqtt': forward_serial_to_mqtt,
  183. 'mqtt_to_serial': forward_mqtt_to_serial,
  184. 'publish_topic': mqtt_publish_topic
  185. })
  186. except Exception as e:
  187. logger.error(f"处理状态命名空间连接时出错: {str(e)}")
  188. @socketio.on('connect', namespace=SOCKETIO_NAMESPACE_CONTROL)
  189. def handle_control_connect():
  190. """处理控制命名空间的连接"""
  191. try:
  192. client_id = str(uuid.uuid4())
  193. connected_clients['control'].add(client_id)
  194. logger.info(f'客户端已连接到控制命名空间,当前连接数: {len(connected_clients["control"])}')
  195. except Exception as e:
  196. logger.error(f"处理控制命名空间连接时出错: {str(e)}")
  197. @socketio.on('disconnect', namespace=SOCKETIO_NAMESPACE_DATA)
  198. def handle_data_disconnect():
  199. """处理数据命名空间的断开连接"""
  200. try:
  201. # 清理客户端连接记录
  202. # 在实际应用中,可能需要更复杂的逻辑来追踪具体哪个客户端断开了连接
  203. if len(connected_clients['data']) > 0:
  204. # 这里简化处理,实际应该维护session到client_id的映射
  205. connected_clients['data'].pop() # 注意:这是一个简化的实现
  206. logger.info(f'客户端已断开数据命名空间的连接,当前连接数: {len(connected_clients["data"])}')
  207. except Exception as e:
  208. logger.error(f"处理数据命名空间断开连接时出错: {str(e)}")
  209. @socketio.on('disconnect', namespace=SOCKETIO_NAMESPACE_STATUS)
  210. def handle_status_disconnect():
  211. """处理状态命名空间的断开连接"""
  212. try:
  213. # 清理客户端连接记录
  214. if len(connected_clients['status']) > 0:
  215. connected_clients['status'].pop()
  216. logger.info(f'客户端已断开状态命名空间的连接,当前连接数: {len(connected_clients["status"])}')
  217. except Exception as e:
  218. logger.error(f"处理状态命名空间断开连接时出错: {str(e)}")
  219. @socketio.on('disconnect', namespace=SOCKETIO_NAMESPACE_CONTROL)
  220. def handle_control_disconnect():
  221. """处理控制命名空间的断开连接"""
  222. try:
  223. # 清理客户端连接记录
  224. if len(connected_clients['control']) > 0:
  225. connected_clients['control'].pop()
  226. logger.info(f'客户端已断开控制命名空间的连接,当前连接数: {len(connected_clients["control"])}')
  227. except Exception as e:
  228. logger.error(f"处理控制命名空间断开连接时出错: {str(e)}")
  229. @socketio.on('serial_send', namespace=SOCKETIO_NAMESPACE_CONTROL)
  230. def handle_serial_send(data):
  231. """通过WebSocket处理串口发送数据请求"""
  232. try:
  233. message = data.get('message', '')
  234. if not message:
  235. emit('serial_send_response', {
  236. 'success': False,
  237. 'message': '消息内容不能为空',
  238. 'error_code': ERROR_CODES['CONFIG_ERROR']
  239. })
  240. return
  241. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  242. emit('serial_send_response', {
  243. 'success': False,
  244. 'message': '串口未连接',
  245. 'error_code': ERROR_CODES['SERIAL_CONNECTION_ERROR']
  246. })
  247. return
  248. success, msg = serial_client.send_data(message)
  249. emit('serial_send_response', {
  250. 'success': success,
  251. 'message': msg,
  252. 'error_code': ERROR_CODES['SUCCESS'] if success else ERROR_CODES['SERIAL_SEND_ERROR']
  253. })
  254. if success:
  255. logger.info(f"通过WebSocket发送串口数据成功: {message[:50]}..." if len(message) > 50 else message)
  256. except Exception as e:
  257. error_msg = f"处理串口发送请求时出错: {str(e)}"
  258. logger.error(error_msg)
  259. emit('serial_send_response', {
  260. 'success': False,
  261. 'message': error_msg,
  262. 'error_code': ERROR_CODES['UNKNOWN_ERROR']
  263. })
  264. @socketio.on('mqtt_publish', namespace=SOCKETIO_NAMESPACE_CONTROL)
  265. def handle_mqtt_publish(data):
  266. """通过WebSocket处理MQTT发布数据请求"""
  267. try:
  268. topic = data.get('topic', '')
  269. message = data.get('message', '')
  270. if not topic or not message:
  271. emit('mqtt_publish_response', {
  272. 'success': False,
  273. 'message': '主题或消息内容不能为空',
  274. 'error_code': ERROR_CODES['CONFIG_ERROR']
  275. })
  276. return
  277. if not mqtt_client.get_status():
  278. emit('mqtt_publish_response', {
  279. 'success': False,
  280. 'message': 'MQTT未连接',
  281. 'error_code': ERROR_CODES['MQTT_CONNECTION_ERROR']
  282. })
  283. return
  284. success, msg = mqtt_client.publish(topic, message)
  285. emit('mqtt_publish_response', {
  286. 'success': success,
  287. 'message': msg,
  288. 'error_code': ERROR_CODES['SUCCESS'] if success else ERROR_CODES['MQTT_PUBLISH_ERROR']
  289. })
  290. if success:
  291. logger.info(f"通过WebSocket发布MQTT消息成功: 主题={topic}, 消息={message[:50]}..." if len(message) > 50 else message)
  292. except Exception as e:
  293. error_msg = f"处理MQTT发布请求时出错: {str(e)}"
  294. logger.error(error_msg)
  295. emit('mqtt_publish_response', {
  296. 'success': False,
  297. 'message': error_msg,
  298. 'error_code': ERROR_CODES['UNKNOWN_ERROR']
  299. })
  300. @socketio.on('update_forward_config', namespace=SOCKETIO_NAMESPACE_CONTROL)
  301. def handle_update_forward_config(data):
  302. """通过WebSocket更新转发配置"""
  303. global forward_serial_to_mqtt, forward_mqtt_to_serial, mqtt_publish_topic
  304. try:
  305. # 更新转发标志
  306. if 'serial_to_mqtt' in data:
  307. forward_serial_to_mqtt = bool(data['serial_to_mqtt'])
  308. if 'mqtt_to_serial' in data:
  309. forward_mqtt_to_serial = bool(data['mqtt_to_serial'])
  310. if 'publish_topic' in data:
  311. new_topic = str(data['publish_topic'])
  312. if not new_topic.strip():
  313. raise ValueError("发布主题不能为空")
  314. mqtt_publish_topic = new_topic
  315. # 广播配置更新
  316. socketio.emit('forward_status', {
  317. 'serial_to_mqtt': forward_serial_to_mqtt,
  318. 'mqtt_to_serial': forward_mqtt_to_serial,
  319. 'publish_topic': mqtt_publish_topic
  320. }, namespace=SOCKETIO_NAMESPACE_STATUS)
  321. emit('update_forward_config_response', {
  322. 'success': True,
  323. 'message': '转发配置已更新',
  324. 'error_code': ERROR_CODES['SUCCESS']
  325. })
  326. logger.info(f"转发配置已更新 - 串口到MQTT: {forward_serial_to_mqtt}, MQTT到串口: {forward_mqtt_to_serial}, 发布主题: {mqtt_publish_topic}")
  327. except ValueError as e:
  328. error_msg = str(e)
  329. logger.warning(f"转发配置更新失败: {error_msg}")
  330. emit('update_forward_config_response', {
  331. 'success': False,
  332. 'message': error_msg,
  333. 'error_code': ERROR_CODES['CONFIG_ERROR']
  334. })
  335. except Exception as e:
  336. error_msg = f'更新转发配置失败: {str(e)}'
  337. logger.error(error_msg)
  338. emit('update_forward_config_response', {
  339. 'success': False,
  340. 'message': error_msg,
  341. 'error_code': ERROR_CODES['UNKNOWN_ERROR']
  342. })
  343. @socketio.on('clear_data_buffer', namespace=SOCKETIO_NAMESPACE_CONTROL)
  344. def handle_clear_data_buffer(data):
  345. """通过WebSocket清空数据缓冲区"""
  346. try:
  347. buffer_type = data.get('type', '')
  348. if buffer_type == 'serial':
  349. serial_data_buffer.clear()
  350. emit('clear_data_buffer_response', {
  351. 'success': True,
  352. 'message': '串口数据缓冲区已清空',
  353. 'error_code': ERROR_CODES['SUCCESS']
  354. })
  355. logger.info("串口数据缓冲区已清空")
  356. elif buffer_type == 'mqtt':
  357. mqtt_data_buffer.clear()
  358. emit('clear_data_buffer_response', {
  359. 'success': True,
  360. 'message': 'MQTT数据缓冲区已清空',
  361. 'error_code': ERROR_CODES['SUCCESS']
  362. })
  363. logger.info("MQTT数据缓冲区已清空")
  364. elif buffer_type == 'all':
  365. serial_data_buffer.clear()
  366. mqtt_data_buffer.clear()
  367. emit('clear_data_buffer_response', {
  368. 'success': True,
  369. 'message': '所有数据缓冲区已清空',
  370. 'error_code': ERROR_CODES['SUCCESS']
  371. })
  372. logger.info("所有数据缓冲区已清空")
  373. else:
  374. emit('clear_data_buffer_response', {
  375. 'success': False,
  376. 'message': '无效的缓冲区类型',
  377. 'error_code': ERROR_CODES['CONFIG_ERROR']
  378. })
  379. logger.warning(f"清空缓冲区失败: 无效的缓冲区类型 '{buffer_type}'")
  380. except Exception as e:
  381. error_msg = f'清空缓冲区失败: {str(e)}'
  382. logger.error(error_msg)
  383. emit('clear_data_buffer_response', {
  384. 'success': False,
  385. 'message': error_msg,
  386. 'error_code': ERROR_CODES['UNKNOWN_ERROR']
  387. })
  388. # 设置回调
  389. serial_client.set_data_callback(serial_data_handler)
  390. serial_client.set_status_callback(serial_status_handler)
  391. mqtt_client.set_data_callback(mqtt_data_handler)
  392. mqtt_client.set_status_callback(mqtt_status_handler)
  393. # 设备配置文件路径
  394. DEVICE_CONFIG_FILE = '/root/dzxj_dtu/devices.json'
  395. confirm_loop_running = False
  396. def save_device_config():
  397. """保存设备配置到文件"""
  398. try:
  399. filepath = DEVICE_CONFIG_FILE
  400. success = address_config.save_config(filepath)
  401. if success:
  402. logger.info(f"设备配置已保存到: {filepath}")
  403. except Exception as e:
  404. logger.error(f"保存设备配置失败: {str(e)}")
  405. def load_device_config():
  406. """加载设备配置"""
  407. try:
  408. filepath = DEVICE_CONFIG_FILE
  409. if os.path.exists(filepath):
  410. success = address_config.load_config(filepath)
  411. if success:
  412. devices = address_config.get_stored_devices()
  413. logger.info(f"已加载 {len(devices)} 个设备配置")
  414. return devices
  415. except Exception as e:
  416. logger.error(f"加载设备配置失败: {str(e)}")
  417. return {}
  418. def confirm_loop():
  419. """后台确认线程:每10秒对所有已存储设备发送 confirm_address"""
  420. global confirm_loop_running
  421. confirm_loop_running = True
  422. logger.info("启动设备确认线程 (间隔10秒)")
  423. while confirm_loop_running:
  424. try:
  425. devices = address_config.get_stored_devices()
  426. if not devices:
  427. time.sleep(10)
  428. continue
  429. if not serial_client.get_status():
  430. time.sleep(10)
  431. continue
  432. _st = serial_client.get_status()
  433. if not (isinstance(_st, dict) and _st.get('connected', False)):
  434. time.sleep(10)
  435. continue
  436. for uid_hex, addr in devices.items():
  437. try:
  438. uid_bytes = bytes.fromhex(uid_hex)
  439. cmd = build_confirm_address(addr, uid_bytes)
  440. success, msg = serial_client.send_raw(cmd)
  441. if success:
  442. logger.debug(f"确认设备: 地址={addr}, UID={uid_hex[:16]}...")
  443. else:
  444. logger.warning(f"确认失败 地址={addr}: {msg}")
  445. except Exception as e:
  446. logger.error(f"确认异常 地址={addr}: {str(e)}")
  447. time.sleep(0.1)
  448. except Exception as e:
  449. logger.error(f"确认线程异常: {str(e)}")
  450. time.sleep(10)
  451. # API路由
  452. # 移除静态文件服务,前端由nginx提供服务
  453. # 根路径路由
  454. @app.route('/')
  455. def index():
  456. return send_from_directory(STATIC_FOLDER, 'index.html')
  457. # 404错误处理器 - 解决SPA路由刷新问题
  458. @app.errorhandler(404)
  459. def page_not_found(e):
  460. """处理所有404错误,对于非API路径返回index.html"""
  461. path = request.path
  462. # 检查是否是API请求
  463. if path.startswith('/api/'):
  464. # 对于API请求,返回404错误
  465. return jsonify({
  466. 'success': False,
  467. 'message': 'API endpoint not found'
  468. }), 404
  469. # 对于所有非API路径,返回index.html让前端路由处理
  470. return send_from_directory(STATIC_FOLDER, 'index.html'), 200
  471. @app.route('/api/serial/ports', methods=['GET'])
  472. def get_serial_ports():
  473. """获取可用串口列表"""
  474. ports = serial_client.list_ports()
  475. return jsonify({
  476. 'success': True,
  477. 'ports': ports
  478. })
  479. @app.route('/api/serial/connect', methods=['POST'])
  480. def serial_connect():
  481. """连接串口"""
  482. try:
  483. data = request.json
  484. port = data.get('port')
  485. baudrate = data.get('baudrate', 9600)
  486. bytesize = data.get('bytesize', 8)
  487. parity = data.get('parity', 'N')
  488. stopbits = data.get('stopbits', 1)
  489. timeout = data.get('timeout', 0.1)
  490. if not port:
  491. logger.warning("连接串口请求缺少串口名称")
  492. return jsonify({
  493. 'success': False,
  494. 'message': '串口名称不能为空',
  495. 'error_code': ERROR_CODES['CONFIG_ERROR']
  496. }), 400
  497. # 先断开之前的连接
  498. _status = serial_client.get_status()
  499. if isinstance(_status, dict) and _status.get('connected', False):
  500. _port = serial_client.current_config.port if serial_client.current_config else 'unknown'
  501. logger.info(f"断开现有串口连接: {_port}")
  502. serial_client.disconnect()
  503. # 连接新的串口
  504. logger.info(f"尝试连接串口: {port}, 波特率: {baudrate}")
  505. success, message = serial_client.connect(port, baudrate=baudrate, timeout=timeout, bytesize=bytesize, parity=parity, stopbits=stopbits)
  506. status_code = 200 if success else 400
  507. error_code = ERROR_CODES['SUCCESS'] if success else ERROR_CODES['SERIAL_CONNECTION_ERROR']
  508. response = {
  509. 'success': success,
  510. 'message': message,
  511. 'error_code': error_code
  512. }
  513. if success:
  514. logger.info(f"串口连接成功: {port}")
  515. else:
  516. logger.error(f"串口连接失败: {message}")
  517. return jsonify(response), status_code
  518. except Exception as e:
  519. error_msg = f'连接串口时出错: {str(e)}'
  520. logger.exception(error_msg) # 使用exception记录完整堆栈
  521. return jsonify({
  522. 'success': False,
  523. 'message': error_msg,
  524. 'error_code': ERROR_CODES['UNKNOWN_ERROR']
  525. }), 500
  526. @app.route('/api/serial/disconnect', methods=['POST'])
  527. def serial_disconnect():
  528. """断开串口连接"""
  529. success, message = serial_client.disconnect()
  530. return jsonify({
  531. 'success': success,
  532. 'message': message
  533. })
  534. @app.route('/api/serial/status', methods=['GET'])
  535. def serial_get_status():
  536. """获取串口状态"""
  537. _st = serial_client.get_status()
  538. return jsonify({
  539. 'connected': _st.get('connected', False) if isinstance(_st, dict) else bool(_st)
  540. })
  541. @app.route('/api/serial/send', methods=['POST'])
  542. def serial_send():
  543. """发送数据到串口"""
  544. data = request.json
  545. message = data.get('message')
  546. if not message:
  547. return jsonify({
  548. 'success': False,
  549. 'message': '消息内容不能为空'
  550. })
  551. success, message = serial_client.send_data(message)
  552. return jsonify({
  553. 'success': success,
  554. 'message': message
  555. })
  556. @app.route('/api/mqtt/connect', methods=['POST'])
  557. def mqtt_connect():
  558. """连接MQTT服务器"""
  559. try:
  560. data = request.json
  561. host = data.get('broker') or data.get('host', 'localhost')
  562. port = data.get('port', 1883)
  563. client_id = data.get('client_id', f'serial_gateway_{int(time.time())}')
  564. username = data.get('username')
  565. password = data.get('password')
  566. keepalive = data.get('keepalive', 60)
  567. # 先断开之前的连接
  568. if mqtt_client.get_status():
  569. logger.info(f"断开现有MQTT连接: {mqtt_client.host}:{mqtt_client.port}")
  570. mqtt_client.disconnect()
  571. # 连接新的MQTT服务器
  572. logger.info(f"尝试连接MQTT服务器: {host}:{port}, 客户端ID: {client_id}")
  573. success, message = mqtt_client.connect(
  574. host=host,
  575. port=port,
  576. client_id=client_id,
  577. username=username,
  578. password=password,
  579. keepalive=keepalive
  580. )
  581. # 如果连接成功,订阅主题
  582. if success and 'topics' in data:
  583. mqtt_client.subscribe(data['topics'])
  584. status_code = 200 if success else 400
  585. error_code = ERROR_CODES['SUCCESS'] if success else ERROR_CODES['MQTT_CONNECTION_ERROR']
  586. response = {
  587. 'success': success,
  588. 'message': message,
  589. 'error_code': error_code
  590. }
  591. if success:
  592. logger.info(f"MQTT服务器连接成功: {host}:{port}")
  593. else:
  594. logger.error(f"MQTT服务器连接失败: {message}")
  595. return jsonify(response), status_code
  596. except Exception as e:
  597. error_msg = f'连接MQTT服务器时出错: {str(e)}'
  598. logger.exception(error_msg)
  599. return jsonify({
  600. 'success': False,
  601. 'message': error_msg,
  602. 'error_code': ERROR_CODES['UNKNOWN_ERROR']
  603. }), 500
  604. @app.route('/api/mqtt/disconnect', methods=['POST'])
  605. def mqtt_disconnect():
  606. """断开MQTT连接"""
  607. success, message = mqtt_client.disconnect()
  608. return jsonify({
  609. 'success': success,
  610. 'message': message
  611. })
  612. @app.route('/api/mqtt/status', methods=['GET'])
  613. def mqtt_get_status():
  614. """获取MQTT状态"""
  615. return jsonify({
  616. 'connected': mqtt_client.get_status()
  617. })
  618. @app.route('/api/mqtt/publish', methods=['POST'])
  619. def mqtt_publish():
  620. """发布MQTT消息"""
  621. data = request.json
  622. topic = data.get('topic')
  623. message = data.get('message')
  624. if not topic or not message:
  625. return jsonify({
  626. 'success': False,
  627. 'message': '主题和消息内容不能为空'
  628. })
  629. success, message = mqtt_client.publish(topic, message)
  630. return jsonify({
  631. 'success': success,
  632. 'message': message
  633. })
  634. @app.route('/api/mqtt/subscribe', methods=['POST'])
  635. def mqtt_subscribe():
  636. """订阅MQTT主题"""
  637. data = request.json
  638. topics = data.get('topics', [])
  639. if not topics:
  640. return jsonify({
  641. 'success': False,
  642. 'message': '请至少订阅一个主题'
  643. })
  644. success, message = mqtt_client.subscribe(topics)
  645. return jsonify({
  646. 'success': success,
  647. 'message': message
  648. })
  649. @app.route('/api/data/serial', methods=['GET'])
  650. def get_serial_data():
  651. """获取串口数据"""
  652. return jsonify({
  653. 'data': serial_data_buffer
  654. })
  655. @app.route('/api/data/mqtt', methods=['GET'])
  656. def get_mqtt_data():
  657. """获取MQTT数据"""
  658. return jsonify({
  659. 'data': mqtt_data_buffer
  660. })
  661. @app.route('/api/forward/config', methods=['POST'])
  662. def set_forward_config():
  663. """设置转发配置"""
  664. global forward_serial_to_mqtt, forward_mqtt_to_serial, mqtt_publish_topic
  665. data = request.json
  666. forward_serial_to_mqtt = data.get('serial_to_mqtt', False)
  667. forward_mqtt_to_serial = data.get('mqtt_to_serial', False)
  668. if 'publish_topic' in data:
  669. mqtt_publish_topic = data['publish_topic']
  670. return jsonify({
  671. 'success': True,
  672. 'message': '转发配置已更新',
  673. 'config': {
  674. 'serial_to_mqtt': forward_serial_to_mqtt,
  675. 'mqtt_to_serial': forward_mqtt_to_serial,
  676. 'publish_topic': mqtt_publish_topic
  677. }
  678. })
  679. @app.route('/api/forward/status', methods=['GET'])
  680. def get_forward_status():
  681. """获取转发状态"""
  682. return jsonify({
  683. 'serial_to_mqtt': forward_serial_to_mqtt,
  684. 'mqtt_to_serial': forward_mqtt_to_serial,
  685. 'publish_topic': mqtt_publish_topic
  686. })
  687. # 健康检查端点
  688. @app.route('/api/health', methods=['GET'])
  689. def health_check():
  690. """健康检查端点"""
  691. try:
  692. # 获取客户端连接数量
  693. client_counts = {
  694. 'data': len(connected_clients['data']),
  695. 'status': len(connected_clients['status']),
  696. 'control': len(connected_clients['control'])
  697. }
  698. # 获取缓冲区大小
  699. buffer_sizes = {
  700. 'serial': len(serial_data_buffer),
  701. 'mqtt': len(mqtt_data_buffer)
  702. }
  703. # 执行系统负载检查
  704. # 注意:这只是一个简化的负载检查,实际应用中可能需要更复杂的监控
  705. is_healthy = True
  706. load_warnings = []
  707. # 检查缓冲区是否过大
  708. if buffer_sizes['serial'] > MAX_BUFFER_SIZE * 0.8:
  709. is_healthy = False
  710. load_warnings.append(f"串口缓冲区接近最大容量: {buffer_sizes['serial']}/{MAX_BUFFER_SIZE}")
  711. if buffer_sizes['mqtt'] > MAX_BUFFER_SIZE * 0.8:
  712. is_healthy = False
  713. load_warnings.append(f"MQTT缓冲区接近最大容量: {buffer_sizes['mqtt']}/{MAX_BUFFER_SIZE}")
  714. # 检查WebSocket连接数是否过多
  715. total_clients = sum(client_counts.values())
  716. if total_clients > 100: # 设置合理的阈值
  717. is_healthy = False
  718. load_warnings.append(f"WebSocket连接数过多: {total_clients}")
  719. # 尝试获取网络状态信息(使用try-except包装,防止网络模块出错导致健康检查失败)
  720. network_info = None
  721. try:
  722. network_info = network_manager.get_network_status()
  723. except Exception as e:
  724. logger.warning(f"获取网络状态时出错: {str(e)}")
  725. # 不影响整体健康检查,只添加警告
  726. load_warnings.append(f"网络状态获取失败: {str(e)}")
  727. response = {
  728. 'status': 'healthy' if is_healthy else 'warning',
  729. 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
  730. 'services': {
  731. 'serial': serial_status,
  732. 'mqtt': mqtt_status,
  733. 'websocket': True
  734. },
  735. 'client_counts': client_counts,
  736. 'buffer_sizes': buffer_sizes,
  737. 'warnings': load_warnings
  738. }
  739. # 仅当获取到网络信息时添加
  740. if network_info:
  741. response['network'] = network_info
  742. return jsonify(response), 200
  743. except Exception as e:
  744. # 捕获所有异常,确保健康检查不会返回500错误
  745. logger.error(f"健康检查端点出错: {str(e)}")
  746. # 返回一个基础的健康状态,至少显示服务在运行
  747. return jsonify({
  748. 'status': 'error',
  749. 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
  750. 'error': str(e),
  751. 'services': {
  752. 'api': True, # API服务本身是运行的
  753. 'serial': None,
  754. 'mqtt': None,
  755. 'websocket': None
  756. }
  757. }), 200 # 仍然返回200,避免健康检查导致的连锁反应
  758. # 网络配置相关API
  759. @app.route('/api/network/config', methods=['GET'])
  760. def get_network_config():
  761. """获取当前网络配置"""
  762. try:
  763. config = network_manager.get_network_config()
  764. return jsonify(config)
  765. except Exception as e:
  766. logger.error(f'获取网络配置失败: {str(e)}')
  767. return jsonify({'success': False, 'message': f'获取网络配置失败: {str(e)}'}), 500
  768. @app.route('/api/network/config', methods=['POST'])
  769. def update_network_config():
  770. """更新网络配置"""
  771. try:
  772. config_data = request.json
  773. result = network_manager.update_network_config(config_data)
  774. if result['success']:
  775. return jsonify(result)
  776. else:
  777. return jsonify(result), 400
  778. except Exception as e:
  779. logger.error(f'更新网络配置失败: {str(e)}')
  780. return jsonify({'success': False, 'message': f'更新网络配置失败: {str(e)}'}), 500
  781. @app.route('/api/network/status', methods=['GET'])
  782. def get_network_status():
  783. """获取网络状态信息"""
  784. try:
  785. status = network_manager.get_network_status()
  786. return jsonify(status)
  787. except Exception as e:
  788. logger.error(f'获取网络状态失败: {str(e)}')
  789. return jsonify({'success': False, 'message': f'获取网络状态失败: {str(e)}'}), 500
  790. @app.route('/api/network/restart', methods=['POST'])
  791. def restart_network_service():
  792. """重启网络服务以应用新配置"""
  793. try:
  794. result = network_manager.restart_network_service()
  795. if result['success']:
  796. return jsonify(result)
  797. else:
  798. return jsonify(result), 500
  799. except Exception as e:
  800. logger.error(f'重启网络服务失败: {str(e)}')
  801. return jsonify({'success': False, 'message': f'重启网络服务失败: {str(e)}'}), 500
  802. # Modbus RTU API
  803. @app.route('/api/modbus/antenna_addresses', methods=['GET'])
  804. def get_antenna_addresses():
  805. """获取天线地址映射表"""
  806. return jsonify({
  807. 'success': True,
  808. 'antennas': {str(k): f"0x{v:04x}" for k, v in ANTENNA_ADDRESSES.items()}
  809. })
  810. @app.route('/api/modbus/read_antenna', methods=['POST'])
  811. def modbus_read_antenna():
  812. """读取指定天线的卡号
  813. 请求参数:
  814. {
  815. "device_address": 1, // 设备地址 (1-247)
  816. "antenna": 1, // 天线编号 (1-24)
  817. "timeout": 1.0 // 可选,超时时间(秒)
  818. }
  819. """
  820. try:
  821. data = request.json
  822. device_address = data.get('device_address', 1)
  823. antenna = data.get('antenna', 1)
  824. timeout = data.get('timeout')
  825. if antenna < 1 or antenna > 24:
  826. return jsonify({
  827. 'success': False,
  828. 'message': f'无效的天线编号: {antenna}, 必须是1-24'
  829. }), 400
  830. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  831. return jsonify({
  832. 'success': False,
  833. 'message': '串口未连接'
  834. }), 400
  835. result = modbus_client.read_antenna_card(
  836. device_address=device_address,
  837. antenna_num=antenna,
  838. timeout=timeout
  839. )
  840. if 'error' in result:
  841. return jsonify({
  842. 'success': False,
  843. 'message': result['error'],
  844. 'raw_data': result.get('raw_data', '')
  845. }), 400
  846. return jsonify({
  847. 'success': True,
  848. 'data': result
  849. })
  850. except Exception as e:
  851. logger.error(f'读取天线数据失败: {str(e)}')
  852. return jsonify({
  853. 'success': False,
  854. 'message': str(e)
  855. }), 500
  856. @app.route('/api/modbus/read_registers', methods=['POST'])
  857. def modbus_read_registers():
  858. """读取保持寄存器
  859. 请求参数:
  860. {
  861. "device_address": 1,
  862. "start_address": 2, // 起始地址 (十六进制如0x0002或十进制如2)
  863. "quantity": 4, // 寄存器数量
  864. "timeout": 1.0
  865. }
  866. """
  867. try:
  868. data = request.json
  869. device_address = data.get('device_address', 1)
  870. start_address = data.get('start_address', 0)
  871. quantity = data.get('quantity', 1)
  872. timeout = data.get('timeout')
  873. # 支持十六进制字符串
  874. if isinstance(start_address, str):
  875. start_address = int(start_address, 16)
  876. if isinstance(device_address, str):
  877. device_address = int(device_address, 16)
  878. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  879. return jsonify({
  880. 'success': False,
  881. 'message': '串口未连接'
  882. }), 400
  883. result = modbus_client.read_holding_registers(
  884. device_address=device_address,
  885. start_address=start_address,
  886. quantity=quantity,
  887. timeout=timeout
  888. )
  889. if 'error' in result:
  890. return jsonify({
  891. 'success': False,
  892. 'message': result['error'],
  893. 'raw_data': result.get('raw_data', '')
  894. }), 400
  895. return jsonify({
  896. 'success': True,
  897. 'data': result
  898. })
  899. except Exception as e:
  900. logger.error(f'读取寄存器失败: {str(e)}')
  901. return jsonify({
  902. 'success': False,
  903. 'message': str(e)
  904. }), 500
  905. @app.route('/api/modbus/write_register', methods=['POST'])
  906. def modbus_write_register():
  907. """写单个寄存器
  908. 请求参数:
  909. {
  910. "device_address": 1,
  911. "register_address": 1, // 寄存器地址
  912. "value": 256, // 写入的值
  913. "timeout": 1.0
  914. }
  915. """
  916. try:
  917. data = request.json
  918. device_address = data.get('device_address', 1)
  919. register_address = data.get('register_address', 1)
  920. value = data.get('value', 0)
  921. timeout = data.get('timeout')
  922. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  923. return jsonify({
  924. 'success': False,
  925. 'message':'串口未连接'
  926. }), 400
  927. result = modbus_client.write_single_register(
  928. device_address=device_address,
  929. register_address=register_address,
  930. value=value,
  931. timeout=timeout
  932. )
  933. if 'error' in result:
  934. return jsonify({
  935. 'success': False,
  936. 'message': result['error'],
  937. 'raw_data': result.get('raw_data', '')
  938. }), 400
  939. return jsonify({
  940. 'success': True,
  941. 'data': result
  942. })
  943. except Exception as e:
  944. logger.error(f'写寄存器失败: {str(e)}')
  945. return jsonify({
  946. 'success': False,
  947. 'message': str(e)
  948. }), 500
  949. @app.route('/api/modbus/set_rgb_led', methods=['POST'])
  950. def modbus_set_rgb_led():
  951. """设置RGB灯状态
  952. 请求参数:
  953. {
  954. "device_address": 1,
  955. "led_number": 1, // 灯编号 (1-24)
  956. "color": 1, // 颜色: 0=灭, 1=红灯, 2=绿灯, 3=蓝灯
  957. "timeout": 1.0
  958. }
  959. """
  960. try:
  961. data = request.json
  962. device_address = data.get('device_address', 1)
  963. led_number = data.get('led_number', 1)
  964. color = data.get('color', 0)
  965. timeout = data.get('timeout')
  966. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  967. return jsonify({
  968. 'success': False,
  969. 'message': '串口未连接'
  970. }), 400
  971. result = modbus_client.set_rgb_led(
  972. device_address=device_address,
  973. led_number=led_number,
  974. color=color,
  975. timeout=timeout
  976. )
  977. if 'error' in result:
  978. return jsonify({
  979. 'success': False,
  980. 'message': result['error'],
  981. 'raw_data': result.get('raw_data', '')
  982. }), 400
  983. # 更新 LED 状态追踪
  984. led_states[led_number] = color
  985. return jsonify({
  986. 'success': True,
  987. 'data': result
  988. })
  989. except Exception as e:
  990. logger.error(f'设置RGB灯失败: {str(e)}')
  991. return jsonify({
  992. 'success': False,
  993. 'message': str(e)
  994. }), 500
  995. @app.route('/api/modbus/scan', methods=['POST'])
  996. def modbus_scan_devices():
  997. """扫描在线设备
  998. 请求参数:
  999. {
  1000. "max_address": 247, // 最大设备地址
  1001. "timeout": 0.2 // 单个设备超时时间
  1002. }
  1003. """
  1004. try:
  1005. data = request.json or {}
  1006. max_address = data.get('max_address', 247)
  1007. timeout = data.get('timeout', 0.2)
  1008. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  1009. return jsonify({
  1010. 'success': False,
  1011. 'message': '串口未连接'
  1012. }), 400
  1013. # 异步扫描可能更好,但这里先同步实现
  1014. result = modbus_client.scan_devices(max_address)
  1015. return jsonify({
  1016. 'success': True,
  1017. 'devices': result
  1018. })
  1019. except Exception as e:
  1020. logger.error(f'扫描设备失败: {str(e)}')
  1021. return jsonify({
  1022. 'success': False,
  1023. 'message': str(e)
  1024. }), 500
  1025. # ========== 地址配置协议 API ==========
  1026. @app.route('/api/modbus/broadcast_query', methods=['POST'])
  1027. def modbus_broadcast_query():
  1028. """发送广播查询指令"""
  1029. try:
  1030. data = request.json or {}
  1031. timeout = data.get('timeout')
  1032. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  1033. return jsonify({'success': False, 'message': '串口未连接'}), 400
  1034. responses = address_config.broadcast_query(timeout)
  1035. return jsonify({'success': True, 'responses': responses, 'count': len(responses)})
  1036. except Exception as e:
  1037. logger.error(f'广播查询失败: {str(e)}')
  1038. return jsonify({'success': False, 'message': str(e)}), 500
  1039. @app.route('/api/modbus/auto_configure', methods=['POST'])
  1040. def modbus_auto_configure():
  1041. """自动配置设备地址"""
  1042. try:
  1043. data = request.json or {}
  1044. timeout = data.get('timeout')
  1045. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  1046. return jsonify({'success': False, 'message': '串口未连接'}), 400
  1047. result = address_config.auto_configure(timeout)
  1048. if result.get('success') and (result.get('discovered', 0) > 0 or result.get('confirmed', 0) > 0 or result.get('assigned', 0) > 0):
  1049. save_device_config()
  1050. return jsonify(result)
  1051. except Exception as e:
  1052. logger.error(f'自动配置失败: {str(e)}')
  1053. return jsonify({'success': False, 'message': str(e)}), 500
  1054. @app.route('/api/modbus/stored_devices', methods=['GET'])
  1055. def get_stored_devices():
  1056. """获取已存储的设备列表"""
  1057. return jsonify({'success': True, 'devices': address_config.get_stored_devices()})
  1058. @app.route('/api/modbus/stored_devices', methods=['POST'])
  1059. def add_stored_device():
  1060. """添加已存储的设备"""
  1061. try:
  1062. data = request.json
  1063. uid = data.get('uid', '').lower()
  1064. address = data.get('address', 1)
  1065. if len(uid) != 24:
  1066. return jsonify({'success': False, 'message': 'UID长度必须是24个十六进制字符'}), 400
  1067. address_config.add_stored_device(uid, address)
  1068. save_device_config()
  1069. return jsonify({'success': True, 'message': f'已添加设备: UID={uid}, 地址={address}'})
  1070. except Exception as e:
  1071. return jsonify({'success': False, 'message': str(e)}), 500
  1072. @app.route('/api/modbus/load_config', methods=['POST'])
  1073. def modbus_load_config():
  1074. """从文件加载设备配置"""
  1075. try:
  1076. data = request.json
  1077. filepath = data.get('filepath', '/tmp/modbus_devices.json')
  1078. success = address_config.load_config(filepath)
  1079. return jsonify({'success': success, 'devices': address_config.get_stored_devices()})
  1080. except Exception as e:
  1081. return jsonify({'success': False, 'message': str(e)}), 500
  1082. @app.route('/api/modbus/save_config', methods=['POST'])
  1083. def modbus_save_config():
  1084. """保存设备配置到文件"""
  1085. try:
  1086. data = request.json
  1087. filepath = data.get('filepath', '/tmp/modbus_devices.json')
  1088. success = address_config.save_config(filepath)
  1089. return jsonify({'success': success, 'message': f'配置已保存到: {filepath}' if success else '保存失败'})
  1090. except Exception as e:
  1091. return jsonify({'success': False, 'message': str(e)}), 500
  1092. @app.route('/api/modbus/confirm_devices', methods=['POST'])
  1093. def confirm_devices():
  1094. """手动触发对所有已存储设备的确认"""
  1095. try:
  1096. devices = address_config.get_stored_devices()
  1097. if not devices:
  1098. return jsonify({'success': False, 'message': '没有已存储的设备'}), 400
  1099. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  1100. return jsonify({'success': False, 'message': '串口未连接'}), 400
  1101. results = []
  1102. for uid_hex, addr in devices.items():
  1103. try:
  1104. uid_bytes = bytes.fromhex(uid_hex)
  1105. cmd = build_confirm_address(addr, uid_bytes)
  1106. success, msg = serial_client.send_raw(cmd)
  1107. logger.info(f"确认设备 地址={addr}: {'成功' if success else '失败 ' + msg}")
  1108. results.append({'address': addr, 'uid': uid_hex, 'success': success, 'message': msg})
  1109. except Exception as e:
  1110. results.append({'address': addr, 'uid': uid_hex, 'success': False, 'message': str(e)})
  1111. time.sleep(0.1)
  1112. return jsonify({'success': True, 'results': results, 'count': len(results)})
  1113. except Exception as e:
  1114. return jsonify({'success': False, 'message': str(e)}), 500
  1115. # LED 状态追踪 (内存中跟踪每个灯的最后设置状态)
  1116. led_states = {i: 0 for i in range(1, 25)} # 0=off, 1=red, 2=green, 3=blue
  1117. @app.route('/api/modbus/led_status', methods=['GET'])
  1118. def get_led_status():
  1119. """获取所有 LED 状态"""
  1120. return jsonify({'success': True, 'leds': led_states})
  1121. @app.route('/api/modbus/set_all_leds', methods=['POST'])
  1122. def set_all_leds():
  1123. """批量设置 LED
  1124. 请求: {"device_address": 1, "color": 1}
  1125. 设置所有 24 个 LED 到指定颜色
  1126. """
  1127. try:
  1128. data = request.json
  1129. device_address = data.get('device_address', 1)
  1130. color = data.get('color', 0)
  1131. timeout = data.get('timeout')
  1132. if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
  1133. return jsonify({'success': False, 'message': '串口未连接'}), 400
  1134. results = []
  1135. for led_num in range(1, 25):
  1136. result = modbus_client.set_rgb_led(
  1137. device_address=device_address,
  1138. led_number=led_num,
  1139. color=color,
  1140. timeout=timeout
  1141. )
  1142. if 'error' in result:
  1143. results.append({'led': led_num, 'success': False, 'error': result['error']})
  1144. else:
  1145. results.append({'led': led_num, 'success': True})
  1146. led_states[led_num] = color
  1147. return jsonify({'success': True, 'results': results})
  1148. except Exception as e:
  1149. logger.error(f'批量设置LED失败: {str(e)}')
  1150. return jsonify({'success': False, 'message': str(e)}), 500
  1151. # 在 set_rgb_led 中更新 LED 状态追踪
  1152. # 不再需要静态文件目录,前端由nginx提供服务
  1153. if __name__ == '__main__':
  1154. try:
  1155. # 启动前的初始化工作
  1156. logger.info('启动串口-MQTT网关服务...')
  1157. logger.info(f"配置信息: 主机={FLASK_HOST}, 端口={FLASK_PORT}, 调试模式={FLASK_DEBUG}")
  1158. # 加载设备配置
  1159. loaded = load_device_config()
  1160. logger.info(f"已加载 {len(loaded)} 个设备配置")
  1161. logger.info("设备确认线程已禁用(确认命令干扰运行中设备的Modbus通信)")
  1162. # 启动服务
  1163. socketio.run(
  1164. app,
  1165. host=FLASK_HOST,
  1166. port=FLASK_PORT,
  1167. debug=FLASK_DEBUG,
  1168. use_reloader=False, # 禁用重载器以避免重复初始化问题
  1169. log_output=False, # 禁用Flask的日志输出,使用我们自己的日志配置
  1170. allow_unsafe_werkzeug=True
  1171. )
  1172. except KeyboardInterrupt:
  1173. # 优雅退出
  1174. logger.info('正在关闭应用...')
  1175. try:
  1176. if isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False):
  1177. serial_client.disconnect()
  1178. logger.info('串口连接已断开')
  1179. if mqtt_client.get_status():
  1180. mqtt_client.disconnect()
  1181. logger.info('MQTT连接已断开')
  1182. except Exception as e:
  1183. logger.error(f'关闭连接时出错: {str(e)}')
  1184. # 清理WebSocket连接
  1185. for client_type in connected_clients:
  1186. connected_clients[client_type].clear()
  1187. logger.info('应用已安全关闭')
  1188. except Exception as e:
  1189. logger.exception(f'应用启动失败') # 使用exception记录完整堆栈
  1190. # 确保资源被释放
  1191. try:
  1192. serial_client.disconnect()
  1193. mqtt_client.disconnect()
  1194. except:
  1195. pass