| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399 |
- from flask import Flask, jsonify, request, abort
- from flask_cors import CORS
- from flask_socketio import SocketIO, emit
- import threading
- import time
- import json
- import os
- import logging
- import uuid
- # 导入配置
- from config import (
- MAX_BUFFER_SIZE,
- FLASK_SECRET_KEY,
- FLASK_DEBUG,
- FLASK_HOST,
- FLASK_PORT,
- LOG_LEVEL,
- LOG_FORMAT,
- LOG_FILE,
- SOCKETIO_ASYNC_MODE,
- SOCKETIO_ALLOWED_ORIGINS,
- DEFAULT_FORWARD_SERIAL_TO_MQTT,
- DEFAULT_FORWARD_MQTT_TO_SERIAL,
- DEFAULT_MQTT_PUBLISH_TOPIC,
- SOCKETIO_NAMESPACE_DATA,
- SOCKETIO_NAMESPACE_STATUS,
- SOCKETIO_NAMESPACE_CONTROL,
- ERROR_CODES,
- ERROR_MESSAGES
- )
- # 配置日志
- logging_config = {
- 'level': getattr(logging, LOG_LEVEL),
- 'format': LOG_FORMAT
- }
- if LOG_FILE:
- logging_config['filename'] = LOG_FILE
- logging.basicConfig(**logging_config)
- logger = logging.getLogger('serial_mqtt_gateway')
- from modules.serial_port import SerialPort
- from modules.mqtt_client import MQTTClient
- from modules.network_config import network_manager
- from modules.modbus_rtu import ModbusRTUClient, ANTENNA_ADDRESSES, AddressConfigProtocol, build_broadcast_query, build_confirm_address, build_assign_address
- app = Flask(__name__)
- app.config['SECRET_KEY'] = FLASK_SECRET_KEY
- # 配置CORS以允许nginx代理的前端访问
- CORS(app, resources={r"/api/*": {"origins": "*"}, r"/socket.io/*": {"origins": "*"}})
- # 初始化SocketIO
- socketio = SocketIO(
- app,
- cors_allowed_origins=SOCKETIO_ALLOWED_ORIGINS,
- async_mode=SOCKETIO_ASYNC_MODE,
- manage_session=False, # 禁用会话管理以提高性能
- ping_timeout=30, # 心跳超时时间
- ping_interval=25, # 心跳间隔
- logger=FLASK_DEBUG, # 根据Flask调试模式决定是否记录SocketIO日志
- engineio_logger=FLASK_DEBUG
- )
- # 初始化串口和MQTT客户端
- serial_client = SerialPort()
- mqtt_client = MQTTClient()
- modbus_client = ModbusRTUClient(serial_client)
- address_config = AddressConfigProtocol(serial_client)
- # 转发标志
- forward_serial_to_mqtt = DEFAULT_FORWARD_SERIAL_TO_MQTT
- forward_mqtt_to_serial = DEFAULT_FORWARD_MQTT_TO_SERIAL
- mqtt_publish_topic = DEFAULT_MQTT_PUBLISH_TOPIC
- # 数据存储缓冲区
- serial_data_buffer = []
- mqtt_data_buffer = []
- # 状态标志
- serial_status = False
- mqtt_status = False
- # 客户端连接管理
- connected_clients = {
- 'data': set(),
- 'status': set(),
- 'control': set()
- }
- # 设置回调函数
- def serial_data_handler(data):
- """处理串口接收的数据"""
- try:
- # 添加到缓冲区
- timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
- serial_data_buffer.append({
- 'timestamp': timestamp,
- 'data': data
- })
- # 保持缓冲区大小
- if len(serial_data_buffer) > MAX_BUFFER_SIZE:
- serial_data_buffer.pop(0)
-
- # 通过WebSocket广播数据
- socketio.emit('serial_data', {
- 'timestamp': timestamp,
- 'data': data
- }, namespace=SOCKETIO_NAMESPACE_DATA)
-
- # 如果启用了转发且MQTT已连接,转发数据到MQTT
- if forward_serial_to_mqtt and mqtt_client.get_status():
- success, msg = mqtt_client.publish(mqtt_publish_topic, data)
- if not success:
- logger.warning(f"串口数据转发到MQTT失败: {msg}")
- except Exception as e:
- logger.error(f"处理串口数据时出错: {str(e)}")
- def serial_status_handler(status):
- """处理串口状态变化"""
- try:
- global serial_status
- serial_status = status
-
- # 通过WebSocket广播状态变化
- socketio.emit('serial_status', {
- 'connected': status
- }, namespace=SOCKETIO_NAMESPACE_STATUS)
-
- logger.info(f"串口状态更新: {'已连接' if status else '已断开'}")
- except Exception as e:
- logger.error(f"处理串口状态时出错: {str(e)}")
- def mqtt_data_handler(data):
- """处理MQTT接收的数据"""
- try:
- # 添加到缓冲区
- timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
- mqtt_data_buffer.append({
- 'timestamp': timestamp,
- 'topic': data['topic'],
- 'payload': data['payload']
- })
- # 保持缓冲区大小
- if len(mqtt_data_buffer) > MAX_BUFFER_SIZE:
- mqtt_data_buffer.pop(0)
-
- # 通过WebSocket广播数据
- socketio.emit('mqtt_data', {
- 'timestamp': timestamp,
- 'topic': data['topic'],
- 'payload': data['payload']
- }, namespace=SOCKETIO_NAMESPACE_DATA)
-
- # 如果启用了转发且串口已连接,转发数据到串口
- if forward_mqtt_to_serial and serial_client.get_status():
- success, msg = serial_client.send_data(data['payload'])
- if not success:
- logger.warning(f"MQTT数据转发到串口失败: {msg}")
- except Exception as e:
- logger.error(f"处理MQTT数据时出错: {str(e)}")
- def mqtt_status_handler(status):
- """处理MQTT状态变化"""
- try:
- global mqtt_status
- mqtt_status = status
-
- # 通过WebSocket广播状态变化
- socketio.emit('mqtt_status', {
- 'connected': status
- }, namespace=SOCKETIO_NAMESPACE_STATUS)
-
- logger.info(f"MQTT状态更新: {'已连接' if status else '已断开'}")
- except Exception as e:
- logger.error(f"处理MQTT状态时出错: {str(e)}")
- # WebSocket事件处理
- @socketio.on('connect', namespace=SOCKETIO_NAMESPACE_DATA)
- def handle_data_connect():
- """处理数据命名空间的连接"""
- try:
- client_id = str(uuid.uuid4())
- connected_clients['data'].add(client_id)
- logger.info(f'客户端已连接到数据命名空间,当前连接数: {len(connected_clients["data"])}')
-
- # 发送当前的缓冲区数据,限制发送的历史记录数量
- max_history = 100 # 限制发送的历史记录数量以提高性能
- emit('serial_data_history', {'data': serial_data_buffer[-max_history:]})
- emit('mqtt_data_history', {'data': mqtt_data_buffer[-max_history:]})
-
- # 存储客户端ID以便断开连接时使用
- socketio.start_background_task(target=lambda: None) # 确保上下文可用
- except Exception as e:
- logger.error(f"处理数据命名空间连接时出错: {str(e)}")
- @socketio.on('connect', namespace=SOCKETIO_NAMESPACE_STATUS)
- def handle_status_connect():
- """处理状态命名空间的连接"""
- try:
- client_id = str(uuid.uuid4())
- connected_clients['status'].add(client_id)
- logger.info(f'客户端已连接到状态命名空间,当前连接数: {len(connected_clients["status"])}')
-
- # 发送当前状态
- emit('serial_status', {'connected': serial_status})
- emit('mqtt_status', {'connected': mqtt_status})
- emit('forward_status', {
- 'serial_to_mqtt': forward_serial_to_mqtt,
- 'mqtt_to_serial': forward_mqtt_to_serial,
- 'publish_topic': mqtt_publish_topic
- })
- except Exception as e:
- logger.error(f"处理状态命名空间连接时出错: {str(e)}")
- @socketio.on('connect', namespace=SOCKETIO_NAMESPACE_CONTROL)
- def handle_control_connect():
- """处理控制命名空间的连接"""
- try:
- client_id = str(uuid.uuid4())
- connected_clients['control'].add(client_id)
- logger.info(f'客户端已连接到控制命名空间,当前连接数: {len(connected_clients["control"])}')
- except Exception as e:
- logger.error(f"处理控制命名空间连接时出错: {str(e)}")
- @socketio.on('disconnect', namespace=SOCKETIO_NAMESPACE_DATA)
- def handle_data_disconnect():
- """处理数据命名空间的断开连接"""
- try:
- # 清理客户端连接记录
- # 在实际应用中,可能需要更复杂的逻辑来追踪具体哪个客户端断开了连接
- if len(connected_clients['data']) > 0:
- # 这里简化处理,实际应该维护session到client_id的映射
- connected_clients['data'].pop() # 注意:这是一个简化的实现
- logger.info(f'客户端已断开数据命名空间的连接,当前连接数: {len(connected_clients["data"])}')
- except Exception as e:
- logger.error(f"处理数据命名空间断开连接时出错: {str(e)}")
- @socketio.on('disconnect', namespace=SOCKETIO_NAMESPACE_STATUS)
- def handle_status_disconnect():
- """处理状态命名空间的断开连接"""
- try:
- # 清理客户端连接记录
- if len(connected_clients['status']) > 0:
- connected_clients['status'].pop()
- logger.info(f'客户端已断开状态命名空间的连接,当前连接数: {len(connected_clients["status"])}')
- except Exception as e:
- logger.error(f"处理状态命名空间断开连接时出错: {str(e)}")
- @socketio.on('disconnect', namespace=SOCKETIO_NAMESPACE_CONTROL)
- def handle_control_disconnect():
- """处理控制命名空间的断开连接"""
- try:
- # 清理客户端连接记录
- if len(connected_clients['control']) > 0:
- connected_clients['control'].pop()
- logger.info(f'客户端已断开控制命名空间的连接,当前连接数: {len(connected_clients["control"])}')
- except Exception as e:
- logger.error(f"处理控制命名空间断开连接时出错: {str(e)}")
- @socketio.on('serial_send', namespace=SOCKETIO_NAMESPACE_CONTROL)
- def handle_serial_send(data):
- """通过WebSocket处理串口发送数据请求"""
- try:
- message = data.get('message', '')
- if not message:
- emit('serial_send_response', {
- 'success': False,
- 'message': '消息内容不能为空',
- 'error_code': ERROR_CODES['CONFIG_ERROR']
- })
- return
-
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- emit('serial_send_response', {
- 'success': False,
- 'message': '串口未连接',
- 'error_code': ERROR_CODES['SERIAL_CONNECTION_ERROR']
- })
- return
-
- success, msg = serial_client.send_data(message)
- emit('serial_send_response', {
- 'success': success,
- 'message': msg,
- 'error_code': ERROR_CODES['SUCCESS'] if success else ERROR_CODES['SERIAL_SEND_ERROR']
- })
-
- if success:
- logger.info(f"通过WebSocket发送串口数据成功: {message[:50]}..." if len(message) > 50 else message)
- except Exception as e:
- error_msg = f"处理串口发送请求时出错: {str(e)}"
- logger.error(error_msg)
- emit('serial_send_response', {
- 'success': False,
- 'message': error_msg,
- 'error_code': ERROR_CODES['UNKNOWN_ERROR']
- })
- @socketio.on('mqtt_publish', namespace=SOCKETIO_NAMESPACE_CONTROL)
- def handle_mqtt_publish(data):
- """通过WebSocket处理MQTT发布数据请求"""
- try:
- topic = data.get('topic', '')
- message = data.get('message', '')
-
- if not topic or not message:
- emit('mqtt_publish_response', {
- 'success': False,
- 'message': '主题或消息内容不能为空',
- 'error_code': ERROR_CODES['CONFIG_ERROR']
- })
- return
-
- if not mqtt_client.get_status():
- emit('mqtt_publish_response', {
- 'success': False,
- 'message': 'MQTT未连接',
- 'error_code': ERROR_CODES['MQTT_CONNECTION_ERROR']
- })
- return
-
- success, msg = mqtt_client.publish(topic, message)
- emit('mqtt_publish_response', {
- 'success': success,
- 'message': msg,
- 'error_code': ERROR_CODES['SUCCESS'] if success else ERROR_CODES['MQTT_PUBLISH_ERROR']
- })
-
- if success:
- logger.info(f"通过WebSocket发布MQTT消息成功: 主题={topic}, 消息={message[:50]}..." if len(message) > 50 else message)
- except Exception as e:
- error_msg = f"处理MQTT发布请求时出错: {str(e)}"
- logger.error(error_msg)
- emit('mqtt_publish_response', {
- 'success': False,
- 'message': error_msg,
- 'error_code': ERROR_CODES['UNKNOWN_ERROR']
- })
- @socketio.on('update_forward_config', namespace=SOCKETIO_NAMESPACE_CONTROL)
- def handle_update_forward_config(data):
- """通过WebSocket更新转发配置"""
- global forward_serial_to_mqtt, forward_mqtt_to_serial, mqtt_publish_topic
-
- try:
- # 更新转发标志
- if 'serial_to_mqtt' in data:
- forward_serial_to_mqtt = bool(data['serial_to_mqtt'])
- if 'mqtt_to_serial' in data:
- forward_mqtt_to_serial = bool(data['mqtt_to_serial'])
- if 'publish_topic' in data:
- new_topic = str(data['publish_topic'])
- if not new_topic.strip():
- raise ValueError("发布主题不能为空")
- mqtt_publish_topic = new_topic
-
- # 广播配置更新
- socketio.emit('forward_status', {
- 'serial_to_mqtt': forward_serial_to_mqtt,
- 'mqtt_to_serial': forward_mqtt_to_serial,
- 'publish_topic': mqtt_publish_topic
- }, namespace=SOCKETIO_NAMESPACE_STATUS)
-
- emit('update_forward_config_response', {
- 'success': True,
- 'message': '转发配置已更新',
- 'error_code': ERROR_CODES['SUCCESS']
- })
-
- logger.info(f"转发配置已更新 - 串口到MQTT: {forward_serial_to_mqtt}, MQTT到串口: {forward_mqtt_to_serial}, 发布主题: {mqtt_publish_topic}")
- except ValueError as e:
- error_msg = str(e)
- logger.warning(f"转发配置更新失败: {error_msg}")
- emit('update_forward_config_response', {
- 'success': False,
- 'message': error_msg,
- 'error_code': ERROR_CODES['CONFIG_ERROR']
- })
- except Exception as e:
- error_msg = f'更新转发配置失败: {str(e)}'
- logger.error(error_msg)
- emit('update_forward_config_response', {
- 'success': False,
- 'message': error_msg,
- 'error_code': ERROR_CODES['UNKNOWN_ERROR']
- })
- @socketio.on('clear_data_buffer', namespace=SOCKETIO_NAMESPACE_CONTROL)
- def handle_clear_data_buffer(data):
- """通过WebSocket清空数据缓冲区"""
- try:
- buffer_type = data.get('type', '')
-
- if buffer_type == 'serial':
- serial_data_buffer.clear()
- emit('clear_data_buffer_response', {
- 'success': True,
- 'message': '串口数据缓冲区已清空',
- 'error_code': ERROR_CODES['SUCCESS']
- })
- logger.info("串口数据缓冲区已清空")
- elif buffer_type == 'mqtt':
- mqtt_data_buffer.clear()
- emit('clear_data_buffer_response', {
- 'success': True,
- 'message': 'MQTT数据缓冲区已清空',
- 'error_code': ERROR_CODES['SUCCESS']
- })
- logger.info("MQTT数据缓冲区已清空")
- elif buffer_type == 'all':
- serial_data_buffer.clear()
- mqtt_data_buffer.clear()
- emit('clear_data_buffer_response', {
- 'success': True,
- 'message': '所有数据缓冲区已清空',
- 'error_code': ERROR_CODES['SUCCESS']
- })
- logger.info("所有数据缓冲区已清空")
- else:
- emit('clear_data_buffer_response', {
- 'success': False,
- 'message': '无效的缓冲区类型',
- 'error_code': ERROR_CODES['CONFIG_ERROR']
- })
- logger.warning(f"清空缓冲区失败: 无效的缓冲区类型 '{buffer_type}'")
- except Exception as e:
- error_msg = f'清空缓冲区失败: {str(e)}'
- logger.error(error_msg)
- emit('clear_data_buffer_response', {
- 'success': False,
- 'message': error_msg,
- 'error_code': ERROR_CODES['UNKNOWN_ERROR']
- })
- # 设置回调
- serial_client.set_data_callback(serial_data_handler)
- serial_client.set_status_callback(serial_status_handler)
- mqtt_client.set_data_callback(mqtt_data_handler)
- mqtt_client.set_status_callback(mqtt_status_handler)
- # 设备配置文件路径
- DEVICE_CONFIG_FILE = '/root/dzxj_dtu/devices.json'
- confirm_loop_running = False
- def save_device_config():
- """保存设备配置到文件"""
- try:
- filepath = DEVICE_CONFIG_FILE
- success = address_config.save_config(filepath)
- if success:
- logger.info(f"设备配置已保存到: {filepath}")
- except Exception as e:
- logger.error(f"保存设备配置失败: {str(e)}")
- def load_device_config():
- """加载设备配置"""
- try:
- filepath = DEVICE_CONFIG_FILE
- if os.path.exists(filepath):
- success = address_config.load_config(filepath)
- if success:
- devices = address_config.get_stored_devices()
- logger.info(f"已加载 {len(devices)} 个设备配置")
- return devices
- except Exception as e:
- logger.error(f"加载设备配置失败: {str(e)}")
- return {}
- def confirm_loop():
- """后台确认线程:每10秒对所有已存储设备发送 confirm_address"""
- global confirm_loop_running
- confirm_loop_running = True
- logger.info("启动设备确认线程 (间隔10秒)")
- while confirm_loop_running:
- try:
- devices = address_config.get_stored_devices()
- if not devices:
- time.sleep(10)
- continue
- if not serial_client.get_status():
- time.sleep(10)
- continue
- _st = serial_client.get_status()
- if not (isinstance(_st, dict) and _st.get('connected', False)):
- time.sleep(10)
- continue
- for uid_hex, addr in devices.items():
- try:
- uid_bytes = bytes.fromhex(uid_hex)
- cmd = build_confirm_address(addr, uid_bytes)
- success, msg = serial_client.send_raw(cmd)
- if success:
- logger.debug(f"确认设备: 地址={addr}, UID={uid_hex[:16]}...")
- else:
- logger.warning(f"确认失败 地址={addr}: {msg}")
- except Exception as e:
- logger.error(f"确认异常 地址={addr}: {str(e)}")
- time.sleep(0.1)
- except Exception as e:
- logger.error(f"确认线程异常: {str(e)}")
- time.sleep(10)
- # API路由
- # 移除静态文件服务,前端由nginx提供服务
- # 根路径路由
- @app.route('/')
- def index():
- return send_from_directory(STATIC_FOLDER, 'index.html')
- # 404错误处理器 - 解决SPA路由刷新问题
- @app.errorhandler(404)
- def page_not_found(e):
- """处理所有404错误,对于非API路径返回index.html"""
- path = request.path
- # 检查是否是API请求
- if path.startswith('/api/'):
- # 对于API请求,返回404错误
- return jsonify({
- 'success': False,
- 'message': 'API endpoint not found'
- }), 404
- # 对于所有非API路径,返回index.html让前端路由处理
- return send_from_directory(STATIC_FOLDER, 'index.html'), 200
- @app.route('/api/serial/ports', methods=['GET'])
- def get_serial_ports():
- """获取可用串口列表"""
- ports = serial_client.list_ports()
- return jsonify({
- 'success': True,
- 'ports': ports
- })
- @app.route('/api/serial/connect', methods=['POST'])
- def serial_connect():
- """连接串口"""
- try:
- data = request.json
- port = data.get('port')
- baudrate = data.get('baudrate', 9600)
- bytesize = data.get('bytesize', 8)
- parity = data.get('parity', 'N')
- stopbits = data.get('stopbits', 1)
- timeout = data.get('timeout', 0.1)
-
- if not port:
- logger.warning("连接串口请求缺少串口名称")
- return jsonify({
- 'success': False,
- 'message': '串口名称不能为空',
- 'error_code': ERROR_CODES['CONFIG_ERROR']
- }), 400
-
- # 先断开之前的连接
- _status = serial_client.get_status()
- if isinstance(_status, dict) and _status.get('connected', False):
- _port = serial_client.current_config.port if serial_client.current_config else 'unknown'
- logger.info(f"断开现有串口连接: {_port}")
- serial_client.disconnect()
-
- # 连接新的串口
- logger.info(f"尝试连接串口: {port}, 波特率: {baudrate}")
- success, message = serial_client.connect(port, baudrate=baudrate, timeout=timeout, bytesize=bytesize, parity=parity, stopbits=stopbits)
-
- status_code = 200 if success else 400
- error_code = ERROR_CODES['SUCCESS'] if success else ERROR_CODES['SERIAL_CONNECTION_ERROR']
-
- response = {
- 'success': success,
- 'message': message,
- 'error_code': error_code
- }
-
- if success:
- logger.info(f"串口连接成功: {port}")
- else:
- logger.error(f"串口连接失败: {message}")
-
- return jsonify(response), status_code
- except Exception as e:
- error_msg = f'连接串口时出错: {str(e)}'
- logger.exception(error_msg) # 使用exception记录完整堆栈
- return jsonify({
- 'success': False,
- 'message': error_msg,
- 'error_code': ERROR_CODES['UNKNOWN_ERROR']
- }), 500
- @app.route('/api/serial/disconnect', methods=['POST'])
- def serial_disconnect():
- """断开串口连接"""
- success, message = serial_client.disconnect()
- return jsonify({
- 'success': success,
- 'message': message
- })
- @app.route('/api/serial/status', methods=['GET'])
- def serial_get_status():
- """获取串口状态"""
- _st = serial_client.get_status()
- return jsonify({
- 'connected': _st.get('connected', False) if isinstance(_st, dict) else bool(_st)
- })
- @app.route('/api/serial/send', methods=['POST'])
- def serial_send():
- """发送数据到串口"""
- data = request.json
- message = data.get('message')
-
- if not message:
- return jsonify({
- 'success': False,
- 'message': '消息内容不能为空'
- })
-
- success, message = serial_client.send_data(message)
- return jsonify({
- 'success': success,
- 'message': message
- })
- @app.route('/api/mqtt/connect', methods=['POST'])
- def mqtt_connect():
- """连接MQTT服务器"""
- try:
- data = request.json
- host = data.get('broker') or data.get('host', 'localhost')
- port = data.get('port', 1883)
- client_id = data.get('client_id', f'serial_gateway_{int(time.time())}')
- username = data.get('username')
- password = data.get('password')
- keepalive = data.get('keepalive', 60)
-
- # 先断开之前的连接
- if mqtt_client.get_status():
- logger.info(f"断开现有MQTT连接: {mqtt_client.host}:{mqtt_client.port}")
- mqtt_client.disconnect()
-
- # 连接新的MQTT服务器
- logger.info(f"尝试连接MQTT服务器: {host}:{port}, 客户端ID: {client_id}")
- success, message = mqtt_client.connect(
- host=host,
- port=port,
- client_id=client_id,
- username=username,
- password=password,
- keepalive=keepalive
- )
-
- # 如果连接成功,订阅主题
- if success and 'topics' in data:
- mqtt_client.subscribe(data['topics'])
-
- status_code = 200 if success else 400
- error_code = ERROR_CODES['SUCCESS'] if success else ERROR_CODES['MQTT_CONNECTION_ERROR']
-
- response = {
- 'success': success,
- 'message': message,
- 'error_code': error_code
- }
-
- if success:
- logger.info(f"MQTT服务器连接成功: {host}:{port}")
- else:
- logger.error(f"MQTT服务器连接失败: {message}")
-
- return jsonify(response), status_code
- except Exception as e:
- error_msg = f'连接MQTT服务器时出错: {str(e)}'
- logger.exception(error_msg)
- return jsonify({
- 'success': False,
- 'message': error_msg,
- 'error_code': ERROR_CODES['UNKNOWN_ERROR']
- }), 500
- @app.route('/api/mqtt/disconnect', methods=['POST'])
- def mqtt_disconnect():
- """断开MQTT连接"""
- success, message = mqtt_client.disconnect()
- return jsonify({
- 'success': success,
- 'message': message
- })
- @app.route('/api/mqtt/status', methods=['GET'])
- def mqtt_get_status():
- """获取MQTT状态"""
- return jsonify({
- 'connected': mqtt_client.get_status()
- })
- @app.route('/api/mqtt/publish', methods=['POST'])
- def mqtt_publish():
- """发布MQTT消息"""
- data = request.json
- topic = data.get('topic')
- message = data.get('message')
-
- if not topic or not message:
- return jsonify({
- 'success': False,
- 'message': '主题和消息内容不能为空'
- })
-
- success, message = mqtt_client.publish(topic, message)
- return jsonify({
- 'success': success,
- 'message': message
- })
- @app.route('/api/mqtt/subscribe', methods=['POST'])
- def mqtt_subscribe():
- """订阅MQTT主题"""
- data = request.json
- topics = data.get('topics', [])
-
- if not topics:
- return jsonify({
- 'success': False,
- 'message': '请至少订阅一个主题'
- })
-
- success, message = mqtt_client.subscribe(topics)
- return jsonify({
- 'success': success,
- 'message': message
- })
- @app.route('/api/data/serial', methods=['GET'])
- def get_serial_data():
- """获取串口数据"""
- return jsonify({
- 'data': serial_data_buffer
- })
- @app.route('/api/data/mqtt', methods=['GET'])
- def get_mqtt_data():
- """获取MQTT数据"""
- return jsonify({
- 'data': mqtt_data_buffer
- })
- @app.route('/api/forward/config', methods=['POST'])
- def set_forward_config():
- """设置转发配置"""
- global forward_serial_to_mqtt, forward_mqtt_to_serial, mqtt_publish_topic
-
- data = request.json
- forward_serial_to_mqtt = data.get('serial_to_mqtt', False)
- forward_mqtt_to_serial = data.get('mqtt_to_serial', False)
- if 'publish_topic' in data:
- mqtt_publish_topic = data['publish_topic']
-
- return jsonify({
- 'success': True,
- 'message': '转发配置已更新',
- 'config': {
- 'serial_to_mqtt': forward_serial_to_mqtt,
- 'mqtt_to_serial': forward_mqtt_to_serial,
- 'publish_topic': mqtt_publish_topic
- }
- })
- @app.route('/api/forward/status', methods=['GET'])
- def get_forward_status():
- """获取转发状态"""
- return jsonify({
- 'serial_to_mqtt': forward_serial_to_mqtt,
- 'mqtt_to_serial': forward_mqtt_to_serial,
- 'publish_topic': mqtt_publish_topic
- })
- # 健康检查端点
- @app.route('/api/health', methods=['GET'])
- def health_check():
- """健康检查端点"""
- try:
- # 获取客户端连接数量
- client_counts = {
- 'data': len(connected_clients['data']),
- 'status': len(connected_clients['status']),
- 'control': len(connected_clients['control'])
- }
-
- # 获取缓冲区大小
- buffer_sizes = {
- 'serial': len(serial_data_buffer),
- 'mqtt': len(mqtt_data_buffer)
- }
-
- # 执行系统负载检查
- # 注意:这只是一个简化的负载检查,实际应用中可能需要更复杂的监控
- is_healthy = True
- load_warnings = []
-
- # 检查缓冲区是否过大
- if buffer_sizes['serial'] > MAX_BUFFER_SIZE * 0.8:
- is_healthy = False
- load_warnings.append(f"串口缓冲区接近最大容量: {buffer_sizes['serial']}/{MAX_BUFFER_SIZE}")
-
- if buffer_sizes['mqtt'] > MAX_BUFFER_SIZE * 0.8:
- is_healthy = False
- load_warnings.append(f"MQTT缓冲区接近最大容量: {buffer_sizes['mqtt']}/{MAX_BUFFER_SIZE}")
-
- # 检查WebSocket连接数是否过多
- total_clients = sum(client_counts.values())
- if total_clients > 100: # 设置合理的阈值
- is_healthy = False
- load_warnings.append(f"WebSocket连接数过多: {total_clients}")
-
- # 尝试获取网络状态信息(使用try-except包装,防止网络模块出错导致健康检查失败)
- network_info = None
- try:
- network_info = network_manager.get_network_status()
- except Exception as e:
- logger.warning(f"获取网络状态时出错: {str(e)}")
- # 不影响整体健康检查,只添加警告
- load_warnings.append(f"网络状态获取失败: {str(e)}")
-
- response = {
- 'status': 'healthy' if is_healthy else 'warning',
- 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
- 'services': {
- 'serial': serial_status,
- 'mqtt': mqtt_status,
- 'websocket': True
- },
- 'client_counts': client_counts,
- 'buffer_sizes': buffer_sizes,
- 'warnings': load_warnings
- }
-
- # 仅当获取到网络信息时添加
- if network_info:
- response['network'] = network_info
-
- return jsonify(response), 200
- except Exception as e:
- # 捕获所有异常,确保健康检查不会返回500错误
- logger.error(f"健康检查端点出错: {str(e)}")
- # 返回一个基础的健康状态,至少显示服务在运行
- return jsonify({
- 'status': 'error',
- 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
- 'error': str(e),
- 'services': {
- 'api': True, # API服务本身是运行的
- 'serial': None,
- 'mqtt': None,
- 'websocket': None
- }
- }), 200 # 仍然返回200,避免健康检查导致的连锁反应
- # 网络配置相关API
- @app.route('/api/network/config', methods=['GET'])
- def get_network_config():
- """获取当前网络配置"""
- try:
- config = network_manager.get_network_config()
- return jsonify(config)
- except Exception as e:
- logger.error(f'获取网络配置失败: {str(e)}')
- return jsonify({'success': False, 'message': f'获取网络配置失败: {str(e)}'}), 500
- @app.route('/api/network/config', methods=['POST'])
- def update_network_config():
- """更新网络配置"""
- try:
- config_data = request.json
- result = network_manager.update_network_config(config_data)
- if result['success']:
- return jsonify(result)
- else:
- return jsonify(result), 400
- except Exception as e:
- logger.error(f'更新网络配置失败: {str(e)}')
- return jsonify({'success': False, 'message': f'更新网络配置失败: {str(e)}'}), 500
- @app.route('/api/network/status', methods=['GET'])
- def get_network_status():
- """获取网络状态信息"""
- try:
- status = network_manager.get_network_status()
- return jsonify(status)
- except Exception as e:
- logger.error(f'获取网络状态失败: {str(e)}')
- return jsonify({'success': False, 'message': f'获取网络状态失败: {str(e)}'}), 500
- @app.route('/api/network/restart', methods=['POST'])
- def restart_network_service():
- """重启网络服务以应用新配置"""
- try:
- result = network_manager.restart_network_service()
- if result['success']:
- return jsonify(result)
- else:
- return jsonify(result), 500
- except Exception as e:
- logger.error(f'重启网络服务失败: {str(e)}')
- return jsonify({'success': False, 'message': f'重启网络服务失败: {str(e)}'}), 500
- # Modbus RTU API
- @app.route('/api/modbus/antenna_addresses', methods=['GET'])
- def get_antenna_addresses():
- """获取天线地址映射表"""
- return jsonify({
- 'success': True,
- 'antennas': {str(k): f"0x{v:04x}" for k, v in ANTENNA_ADDRESSES.items()}
- })
- @app.route('/api/modbus/read_antenna', methods=['POST'])
- def modbus_read_antenna():
- """读取指定天线的卡号
- 请求参数:
- {
- "device_address": 1, // 设备地址 (1-247)
- "antenna": 1, // 天线编号 (1-24)
- "timeout": 1.0 // 可选,超时时间(秒)
- }
- """
- try:
- data = request.json
- device_address = data.get('device_address', 1)
- antenna = data.get('antenna', 1)
- timeout = data.get('timeout')
- if antenna < 1 or antenna > 24:
- return jsonify({
- 'success': False,
- 'message': f'无效的天线编号: {antenna}, 必须是1-24'
- }), 400
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- return jsonify({
- 'success': False,
- 'message': '串口未连接'
- }), 400
- result = modbus_client.read_antenna_card(
- device_address=device_address,
- antenna_num=antenna,
- timeout=timeout
- )
- if 'error' in result:
- return jsonify({
- 'success': False,
- 'message': result['error'],
- 'raw_data': result.get('raw_data', '')
- }), 400
- return jsonify({
- 'success': True,
- 'data': result
- })
- except Exception as e:
- logger.error(f'读取天线数据失败: {str(e)}')
- return jsonify({
- 'success': False,
- 'message': str(e)
- }), 500
- @app.route('/api/modbus/read_registers', methods=['POST'])
- def modbus_read_registers():
- """读取保持寄存器
- 请求参数:
- {
- "device_address": 1,
- "start_address": 2, // 起始地址 (十六进制如0x0002或十进制如2)
- "quantity": 4, // 寄存器数量
- "timeout": 1.0
- }
- """
- try:
- data = request.json
- device_address = data.get('device_address', 1)
- start_address = data.get('start_address', 0)
- quantity = data.get('quantity', 1)
- timeout = data.get('timeout')
- # 支持十六进制字符串
- if isinstance(start_address, str):
- start_address = int(start_address, 16)
- if isinstance(device_address, str):
- device_address = int(device_address, 16)
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- return jsonify({
- 'success': False,
- 'message': '串口未连接'
- }), 400
- result = modbus_client.read_holding_registers(
- device_address=device_address,
- start_address=start_address,
- quantity=quantity,
- timeout=timeout
- )
- if 'error' in result:
- return jsonify({
- 'success': False,
- 'message': result['error'],
- 'raw_data': result.get('raw_data', '')
- }), 400
- return jsonify({
- 'success': True,
- 'data': result
- })
- except Exception as e:
- logger.error(f'读取寄存器失败: {str(e)}')
- return jsonify({
- 'success': False,
- 'message': str(e)
- }), 500
- @app.route('/api/modbus/write_register', methods=['POST'])
- def modbus_write_register():
- """写单个寄存器
- 请求参数:
- {
- "device_address": 1,
- "register_address": 1, // 寄存器地址
- "value": 256, // 写入的值
- "timeout": 1.0
- }
- """
- try:
- data = request.json
- device_address = data.get('device_address', 1)
- register_address = data.get('register_address', 1)
- value = data.get('value', 0)
- timeout = data.get('timeout')
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- return jsonify({
- 'success': False,
- 'message':'串口未连接'
- }), 400
- result = modbus_client.write_single_register(
- device_address=device_address,
- register_address=register_address,
- value=value,
- timeout=timeout
- )
- if 'error' in result:
- return jsonify({
- 'success': False,
- 'message': result['error'],
- 'raw_data': result.get('raw_data', '')
- }), 400
- return jsonify({
- 'success': True,
- 'data': result
- })
- except Exception as e:
- logger.error(f'写寄存器失败: {str(e)}')
- return jsonify({
- 'success': False,
- 'message': str(e)
- }), 500
- @app.route('/api/modbus/set_rgb_led', methods=['POST'])
- def modbus_set_rgb_led():
- """设置RGB灯状态
- 请求参数:
- {
- "device_address": 1,
- "led_number": 1, // 灯编号 (1-24)
- "color": 1, // 颜色: 0=灭, 1=红灯, 2=绿灯, 3=蓝灯
- "timeout": 1.0
- }
- """
- try:
- data = request.json
- device_address = data.get('device_address', 1)
- led_number = data.get('led_number', 1)
- color = data.get('color', 0)
- timeout = data.get('timeout')
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- return jsonify({
- 'success': False,
- 'message': '串口未连接'
- }), 400
- result = modbus_client.set_rgb_led(
- device_address=device_address,
- led_number=led_number,
- color=color,
- timeout=timeout
- )
- if 'error' in result:
- return jsonify({
- 'success': False,
- 'message': result['error'],
- 'raw_data': result.get('raw_data', '')
- }), 400
- # 更新 LED 状态追踪
- led_states[led_number] = color
- return jsonify({
- 'success': True,
- 'data': result
- })
- except Exception as e:
- logger.error(f'设置RGB灯失败: {str(e)}')
- return jsonify({
- 'success': False,
- 'message': str(e)
- }), 500
- @app.route('/api/modbus/scan', methods=['POST'])
- def modbus_scan_devices():
- """扫描在线设备
- 请求参数:
- {
- "max_address": 247, // 最大设备地址
- "timeout": 0.2 // 单个设备超时时间
- }
- """
- try:
- data = request.json or {}
- max_address = data.get('max_address', 247)
- timeout = data.get('timeout', 0.2)
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- return jsonify({
- 'success': False,
- 'message': '串口未连接'
- }), 400
- # 异步扫描可能更好,但这里先同步实现
- result = modbus_client.scan_devices(max_address)
- return jsonify({
- 'success': True,
- 'devices': result
- })
- except Exception as e:
- logger.error(f'扫描设备失败: {str(e)}')
- return jsonify({
- 'success': False,
- 'message': str(e)
- }), 500
- # ========== 地址配置协议 API ==========
- @app.route('/api/modbus/broadcast_query', methods=['POST'])
- def modbus_broadcast_query():
- """发送广播查询指令"""
- try:
- data = request.json or {}
- timeout = data.get('timeout')
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- return jsonify({'success': False, 'message': '串口未连接'}), 400
- responses = address_config.broadcast_query(timeout)
- return jsonify({'success': True, 'responses': responses, 'count': len(responses)})
- except Exception as e:
- logger.error(f'广播查询失败: {str(e)}')
- return jsonify({'success': False, 'message': str(e)}), 500
- @app.route('/api/modbus/auto_configure', methods=['POST'])
- def modbus_auto_configure():
- """自动配置设备地址"""
- try:
- data = request.json or {}
- timeout = data.get('timeout')
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- return jsonify({'success': False, 'message': '串口未连接'}), 400
- result = address_config.auto_configure(timeout)
- if result.get('success') and (result.get('discovered', 0) > 0 or result.get('confirmed', 0) > 0 or result.get('assigned', 0) > 0):
- save_device_config()
- return jsonify(result)
- except Exception as e:
- logger.error(f'自动配置失败: {str(e)}')
- return jsonify({'success': False, 'message': str(e)}), 500
- @app.route('/api/modbus/stored_devices', methods=['GET'])
- def get_stored_devices():
- """获取已存储的设备列表"""
- return jsonify({'success': True, 'devices': address_config.get_stored_devices()})
- @app.route('/api/modbus/stored_devices', methods=['POST'])
- def add_stored_device():
- """添加已存储的设备"""
- try:
- data = request.json
- uid = data.get('uid', '').lower()
- address = data.get('address', 1)
- if len(uid) != 24:
- return jsonify({'success': False, 'message': 'UID长度必须是24个十六进制字符'}), 400
- address_config.add_stored_device(uid, address)
- save_device_config()
- return jsonify({'success': True, 'message': f'已添加设备: UID={uid}, 地址={address}'})
- except Exception as e:
- return jsonify({'success': False, 'message': str(e)}), 500
- @app.route('/api/modbus/load_config', methods=['POST'])
- def modbus_load_config():
- """从文件加载设备配置"""
- try:
- data = request.json
- filepath = data.get('filepath', '/tmp/modbus_devices.json')
- success = address_config.load_config(filepath)
- return jsonify({'success': success, 'devices': address_config.get_stored_devices()})
- except Exception as e:
- return jsonify({'success': False, 'message': str(e)}), 500
- @app.route('/api/modbus/save_config', methods=['POST'])
- def modbus_save_config():
- """保存设备配置到文件"""
- try:
- data = request.json
- filepath = data.get('filepath', '/tmp/modbus_devices.json')
- success = address_config.save_config(filepath)
- return jsonify({'success': success, 'message': f'配置已保存到: {filepath}' if success else '保存失败'})
- except Exception as e:
- return jsonify({'success': False, 'message': str(e)}), 500
- @app.route('/api/modbus/confirm_devices', methods=['POST'])
- def confirm_devices():
- """手动触发对所有已存储设备的确认"""
- try:
- devices = address_config.get_stored_devices()
- if not devices:
- return jsonify({'success': False, 'message': '没有已存储的设备'}), 400
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- return jsonify({'success': False, 'message': '串口未连接'}), 400
- results = []
- for uid_hex, addr in devices.items():
- try:
- uid_bytes = bytes.fromhex(uid_hex)
- cmd = build_confirm_address(addr, uid_bytes)
- success, msg = serial_client.send_raw(cmd)
- logger.info(f"确认设备 地址={addr}: {'成功' if success else '失败 ' + msg}")
- results.append({'address': addr, 'uid': uid_hex, 'success': success, 'message': msg})
- except Exception as e:
- results.append({'address': addr, 'uid': uid_hex, 'success': False, 'message': str(e)})
- time.sleep(0.1)
- return jsonify({'success': True, 'results': results, 'count': len(results)})
- except Exception as e:
- return jsonify({'success': False, 'message': str(e)}), 500
- # LED 状态追踪 (内存中跟踪每个灯的最后设置状态)
- led_states = {i: 0 for i in range(1, 25)} # 0=off, 1=red, 2=green, 3=blue
- @app.route('/api/modbus/led_status', methods=['GET'])
- def get_led_status():
- """获取所有 LED 状态"""
- return jsonify({'success': True, 'leds': led_states})
- @app.route('/api/modbus/set_all_leds', methods=['POST'])
- def set_all_leds():
- """批量设置 LED
- 请求: {"device_address": 1, "color": 1}
- 设置所有 24 个 LED 到指定颜色
- """
- try:
- data = request.json
- device_address = data.get('device_address', 1)
- color = data.get('color', 0)
- timeout = data.get('timeout')
- if not (isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False)):
- return jsonify({'success': False, 'message': '串口未连接'}), 400
- results = []
- for led_num in range(1, 25):
- result = modbus_client.set_rgb_led(
- device_address=device_address,
- led_number=led_num,
- color=color,
- timeout=timeout
- )
- if 'error' in result:
- results.append({'led': led_num, 'success': False, 'error': result['error']})
- else:
- results.append({'led': led_num, 'success': True})
- led_states[led_num] = color
- return jsonify({'success': True, 'results': results})
- except Exception as e:
- logger.error(f'批量设置LED失败: {str(e)}')
- return jsonify({'success': False, 'message': str(e)}), 500
- # 在 set_rgb_led 中更新 LED 状态追踪
- # 不再需要静态文件目录,前端由nginx提供服务
- if __name__ == '__main__':
- try:
- # 启动前的初始化工作
- logger.info('启动串口-MQTT网关服务...')
- logger.info(f"配置信息: 主机={FLASK_HOST}, 端口={FLASK_PORT}, 调试模式={FLASK_DEBUG}")
- # 加载设备配置
- loaded = load_device_config()
- logger.info(f"已加载 {len(loaded)} 个设备配置")
- logger.info("设备确认线程已禁用(确认命令干扰运行中设备的Modbus通信)")
-
- # 启动服务
- socketio.run(
- app,
- host=FLASK_HOST,
- port=FLASK_PORT,
- debug=FLASK_DEBUG,
- use_reloader=False, # 禁用重载器以避免重复初始化问题
- log_output=False, # 禁用Flask的日志输出,使用我们自己的日志配置
- allow_unsafe_werkzeug=True
- )
- except KeyboardInterrupt:
- # 优雅退出
- logger.info('正在关闭应用...')
- try:
- if isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False):
- serial_client.disconnect()
- logger.info('串口连接已断开')
- if mqtt_client.get_status():
- mqtt_client.disconnect()
- logger.info('MQTT连接已断开')
- except Exception as e:
- logger.error(f'关闭连接时出错: {str(e)}')
-
- # 清理WebSocket连接
- for client_type in connected_clients:
- connected_clients[client_type].clear()
-
- logger.info('应用已安全关闭')
- except Exception as e:
- logger.exception(f'应用启动失败') # 使用exception记录完整堆栈
- # 确保资源被释放
- try:
- serial_client.disconnect()
- mqtt_client.disconnect()
- except:
- pass
|