|
|
@@ -268,6 +268,13 @@ def mqtt_status_handler(status):
|
|
|
control_topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'control')
|
|
|
mqtt_client.subscribe(control_topic)
|
|
|
logger.info(f"已订阅控制主题: {control_topic}")
|
|
|
+
|
|
|
+ # 订阅广播主题 (DTU发现 7.14 和批量配置 7.15)
|
|
|
+ discover_topic = build_dtu_topic('broadcast', 'dtu', 'discover')
|
|
|
+ config_topic = build_dtu_topic('broadcast', 'dtu', 'config')
|
|
|
+ mqtt_client.subscribe(discover_topic)
|
|
|
+ mqtt_client.subscribe(config_topic)
|
|
|
+ logger.info(f"已订阅广播主题: {discover_topic}, {config_topic}")
|
|
|
except Exception as e:
|
|
|
logger.error(f"处理MQTT状态时出错: {str(e)}")
|
|
|
|
|
|
@@ -329,6 +336,74 @@ def dtu_register():
|
|
|
return False
|
|
|
|
|
|
|
|
|
+def handle_broadcast_discover(payload):
|
|
|
+ """处理广播发现消息 (7.14)
|
|
|
+
|
|
|
+ 服务器通过广播主题发送DISCOVER消息,
|
|
|
+ DTU收到后应立即回应REGISTER消息进行注册
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ logger.info(f"收到广播发现消息: {payload}")
|
|
|
+
|
|
|
+ # 检查消息是否是有效的DISCOVER类型
|
|
|
+ if isinstance(payload, dict):
|
|
|
+ msg_type = payload.get('type', '')
|
|
|
+ if msg_type == 'DISCOVER':
|
|
|
+ # 立即响应注册消息
|
|
|
+ logger.info("收到DISCOVER广播,响应注册消息")
|
|
|
+ dtu_register()
|
|
|
+
|
|
|
+ # 可选:也发送一次状态消息
|
|
|
+ socketio.sleep(0.5)
|
|
|
+ dtu_publish_status()
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"处理广播发现消息失败: {str(e)}")
|
|
|
+
|
|
|
+
|
|
|
+def handle_broadcast_config(payload):
|
|
|
+ """处理广播批量配置消息 (7.15)
|
|
|
+
|
|
|
+ 服务器通过广播主题发送CONFIG消息,
|
|
|
+ 用于批量配置所有在线DTU
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ logger.info(f"收到广播配置消息: {payload}")
|
|
|
+
|
|
|
+ if isinstance(payload, dict):
|
|
|
+ msg_type = payload.get('type', '')
|
|
|
+ if msg_type == 'CONFIG':
|
|
|
+ config_data = payload.get('payload', {})
|
|
|
+
|
|
|
+ # 处理批量配置
|
|
|
+ # 这里可以处理各种配置项,如:
|
|
|
+ # - 上报间隔配置
|
|
|
+ # - 告警阈值配置
|
|
|
+ # - 网络配置等
|
|
|
+ logger.info(f"应用批量配置: {config_data}")
|
|
|
+
|
|
|
+ # 可以在这里添加配置应用逻辑
|
|
|
+ # 例如:更新本地配置、发送确认等
|
|
|
+
|
|
|
+ # 发送配置确认响应
|
|
|
+ response_topic = build_dtu_topic(
|
|
|
+ dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'status'
|
|
|
+ )
|
|
|
+ response_payload = {
|
|
|
+ 'msg_id': f"cfg_ack_{int(time.time() * 1000)}",
|
|
|
+ 'timestamp': int(time.time() * 1000),
|
|
|
+ 'dtu_id': dtu_config['dtu_id'],
|
|
|
+ 'type': 'CONFIG_ACK',
|
|
|
+ 'payload': {
|
|
|
+ 'config_received': True,
|
|
|
+ 'applied_settings': config_data
|
|
|
+ }
|
|
|
+ }
|
|
|
+ mqtt_client.publish(response_topic, json.dumps(response_payload))
|
|
|
+ logger.info("配置确认响应已发送")
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"处理广播配置消息失败: {str(e)}")
|
|
|
+
|
|
|
+
|
|
|
def dtu_publish_status(force=False):
|
|
|
"""发送DTU状态/心跳消息"""
|
|
|
if not mqtt_client.get_status() or not dtu_config.get('enabled'):
|
|
|
@@ -395,6 +470,18 @@ def dtu_publish_event(panel_id, port_id, event_type, jumper_uid, previous_jumper
|
|
|
|
|
|
if success:
|
|
|
logger.info(f"端口事件已发送: {panel_id}:{port_id} - {event_type}")
|
|
|
+ # 记录到事件历史
|
|
|
+ port_event_history.append({
|
|
|
+ 'timestamp': int(time.time() * 1000),
|
|
|
+ 'panel_id': panel_id,
|
|
|
+ 'port_id': port_id,
|
|
|
+ 'event_type': event_type,
|
|
|
+ 'jumper_uid': jumper_uid,
|
|
|
+ 'previous_jumper_uid': previous_jumper_uid
|
|
|
+ })
|
|
|
+ # 限制历史记录数量
|
|
|
+ if len(port_event_history) > 1000:
|
|
|
+ port_event_history[:] = port_event_history[-1000:]
|
|
|
|
|
|
return success
|
|
|
except Exception as e:
|
|
|
@@ -431,6 +518,19 @@ def dtu_publish_alarm(panel_id, port_id, alarm_type, expected_jumper_uid, actual
|
|
|
|
|
|
if success:
|
|
|
logger.warning(f"非法告警已发送: {panel_id}:{port_id} - {alarm_type}")
|
|
|
+ # 记录到事件历史
|
|
|
+ port_event_history.append({
|
|
|
+ 'timestamp': int(time.time() * 1000),
|
|
|
+ 'panel_id': panel_id,
|
|
|
+ 'port_id': port_id,
|
|
|
+ 'event_type': 'ALARM',
|
|
|
+ 'alarm_type': alarm_type,
|
|
|
+ 'expected_jumper_uid': expected_jumper_uid,
|
|
|
+ 'actual_jumper_uid': actual_jumper_uid
|
|
|
+ })
|
|
|
+ # 限制历史记录数量
|
|
|
+ if len(port_event_history) > 1000:
|
|
|
+ port_event_history[:] = port_event_history[-1000:]
|
|
|
|
|
|
return success
|
|
|
except Exception as e:
|
|
|
@@ -519,6 +619,102 @@ def dtu_handle_control(topic, payload):
|
|
|
# 重启DTU(模拟)
|
|
|
response_payload['payload']['result'] = {'message': 'Reboot command received'}
|
|
|
|
|
|
+ elif command == 'READ_PANEL_STATUS':
|
|
|
+ # 读取面板状态
|
|
|
+ # 读取面板的多个寄存器获取状态信息
|
|
|
+ # 寄存器地址定义: 0x0000=运行状态, 0x0001=LED控制, 0x0002-0x0009=天线卡状态
|
|
|
+ device_address = 1
|
|
|
+ for panel_id, cfg in panel_config.items():
|
|
|
+ if panel_id == target:
|
|
|
+ device_address = cfg.get('address', 1)
|
|
|
+ break
|
|
|
+
|
|
|
+ # 读取面板状态寄存器 (地址0x0000开始,读取10个寄存器)
|
|
|
+ status_result = modbus_client.read_holding_registers(device_address, 0x0000, 10)
|
|
|
+
|
|
|
+ # 解析状态
|
|
|
+ registers = status_result.get('registers', [])
|
|
|
+ panel_status = {
|
|
|
+ 'device_address': device_address,
|
|
|
+ 'run_status': registers[0] if len(registers) > 0 else None,
|
|
|
+ 'led_control': registers[1] if len(registers) > 1 else None,
|
|
|
+ 'antenna_status': registers[2:10] if len(registers) >= 10 else [],
|
|
|
+ 'raw_registers': registers
|
|
|
+ }
|
|
|
+
|
|
|
+ response_payload['payload']['success'] = 'error' not in status_result
|
|
|
+ response_payload['payload']['result'] = panel_status
|
|
|
+
|
|
|
+ elif command == 'QUERY_JUMPER_STATUS':
|
|
|
+ # 查询跳线器状态
|
|
|
+ # 跳线器状态寄存器: 0x0100=跳线连接状态, 0x0101=跳线ID高字节, 0x0102=跳线ID低字节
|
|
|
+ device_address = 1
|
|
|
+ for panel_id, cfg in panel_config.items():
|
|
|
+ if panel_id == target:
|
|
|
+ device_address = cfg.get('address', 1)
|
|
|
+ break
|
|
|
+
|
|
|
+ # 读取跳线器状态寄存器 (地址0x0100开始,读取3个寄存器)
|
|
|
+ jumper_result = modbus_client.read_holding_registers(device_address, 0x0100, 3)
|
|
|
+
|
|
|
+ registers = jumper_result.get('registers', [])
|
|
|
+ jumper_status = {
|
|
|
+ 'device_address': device_address,
|
|
|
+ 'connected': bool(registers[0]) if len(registers) > 0 else False,
|
|
|
+ 'jumper_id_high': registers[1] if len(registers) > 1 else 0,
|
|
|
+ 'jumper_id_low': registers[2] if len(registers) > 2 else 0,
|
|
|
+ 'jumper_id': (registers[1] << 16 | registers[2]) if len(registers) > 2 else 0,
|
|
|
+ 'raw_registers': registers
|
|
|
+ }
|
|
|
+
|
|
|
+ response_payload['payload']['success'] = 'error' not in jumper_result
|
|
|
+ response_payload['payload']['result'] = jumper_status
|
|
|
+
|
|
|
+ elif command == 'QUERY_ENV_SENSOR':
|
|
|
+ # 查询环境传感器
|
|
|
+ # 环境传感器寄存器: 0x0200=温度, 0x0201=湿度, 0x0202=光照, 0x0203=烟雾
|
|
|
+ device_address = 1
|
|
|
+ for panel_id, cfg in panel_config.items():
|
|
|
+ if panel_id == target:
|
|
|
+ device_address = cfg.get('address', 1)
|
|
|
+ break
|
|
|
+
|
|
|
+ # 读取环境传感器寄存器 (地址0x0200开始,读取4个寄存器)
|
|
|
+ sensor_result = modbus_client.read_holding_registers(device_address, 0x0200, 4)
|
|
|
+
|
|
|
+ registers = sensor_result.get('registers', [])
|
|
|
+ # 温度和湿度为有符号16位整数,需要除以100转换为实际值
|
|
|
+ env_sensor = {
|
|
|
+ 'device_address': device_address,
|
|
|
+ 'temperature': registers[0] / 100.0 if len(registers) > 0 else None,
|
|
|
+ 'humidity': registers[1] / 100.0 if len(registers) > 1 else None,
|
|
|
+ 'light_level': registers[2] if len(registers) > 2 else None,
|
|
|
+ 'smoke_detected': bool(registers[3]) if len(registers) > 3 else False,
|
|
|
+ 'raw_registers': registers
|
|
|
+ }
|
|
|
+
|
|
|
+ response_payload['payload']['success'] = 'error' not in sensor_result
|
|
|
+ response_payload['payload']['result'] = env_sensor
|
|
|
+
|
|
|
+ elif command == 'OTA_CANCEL':
|
|
|
+ # 取消OTA升级
|
|
|
+ # 向OTA控制寄存器写入取消命令 (0x0000=取消)
|
|
|
+ device_address = 1
|
|
|
+ for panel_id, cfg in panel_config.items():
|
|
|
+ if panel_id == target:
|
|
|
+ device_address = cfg.get('address', 1)
|
|
|
+ break
|
|
|
+
|
|
|
+ # OTA控制寄存器地址: 0x0300
|
|
|
+ # 值: 0x0000=取消升级
|
|
|
+ cancel_result = modbus_client.write_single_register(device_address, 0x0300, 0x0000)
|
|
|
+
|
|
|
+ response_payload['payload']['success'] = 'error' not in cancel_result
|
|
|
+ response_payload['payload']['result'] = {
|
|
|
+ 'message': 'OTA upgrade cancelled' if 'error' not in cancel_result else 'Failed to cancel OTA',
|
|
|
+ 'raw_result': cancel_result
|
|
|
+ }
|
|
|
+
|
|
|
# 发送响应
|
|
|
response_topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'response')
|
|
|
mqtt_client.publish(response_topic, json.dumps(response_payload))
|
|
|
@@ -574,6 +770,33 @@ def mqtt_data_handler_extended(data):
|
|
|
if topic == expected_control_topic:
|
|
|
dtu_handle_control(topic, payload)
|
|
|
|
|
|
+ # 检查是否是状态主题,用于处理OTA状态上报
|
|
|
+ expected_status_topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'status')
|
|
|
+ if topic == expected_status_topic and isinstance(payload, dict):
|
|
|
+ if 'ota_status' in payload or 'firmware_version' in payload:
|
|
|
+ handle_ota_status(dtu_config['dtu_id'], payload)
|
|
|
+
|
|
|
+ # 检查是否是环境传感器数据主题
|
|
|
+ sensor_topic = build_dtu_topic(dtu_config['customer_id'], 'sensor', dtu_config['dtu_id'], 'data')
|
|
|
+ if topic == sensor_topic and isinstance(payload, dict):
|
|
|
+ if 'temperature' in payload or 'humidity' in payload:
|
|
|
+ update_env_sensor_data(
|
|
|
+ payload.get('temperature'),
|
|
|
+ payload.get('humidity'),
|
|
|
+ payload.get('dtu_temperature'),
|
|
|
+ payload.get('sensor_update_time')
|
|
|
+ )
|
|
|
+
|
|
|
+ # 检查是否是广播主题 - DTU发现 (7.14)
|
|
|
+ discover_topic = build_dtu_topic('broadcast', 'dtu', 'discover')
|
|
|
+ if topic == discover_topic:
|
|
|
+ handle_broadcast_discover(payload)
|
|
|
+
|
|
|
+ # 检查是否是广播主题 - 批量配置 (7.15)
|
|
|
+ config_topic = build_dtu_topic('broadcast', 'dtu', 'config')
|
|
|
+ if topic == config_topic:
|
|
|
+ handle_broadcast_config(payload)
|
|
|
+
|
|
|
# 转发到串口(如果启用)
|
|
|
if forward_mqtt_to_serial and serial_client.get_status():
|
|
|
success, msg = serial_client.send_data(payload_str)
|
|
|
@@ -1138,19 +1361,84 @@ 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/mqtt/broadcast_discover', methods=['POST'])
|
|
|
+def mqtt_broadcast_discover():
|
|
|
+ """发送DTU发现广播 (7.14)
|
|
|
+
|
|
|
+ 服务器通过广播主题发送DISCOVER消息,
|
|
|
+ 触发所有在线DTU进行注册响应
|
|
|
+ """
|
|
|
+ data = request.json or {}
|
|
|
+ customer_id = data.get('customer_id', dtu_config.get('customer_id', 'default'))
|
|
|
+
|
|
|
+ discover_topic = build_dtu_topic('broadcast', 'dtu', 'discover')
|
|
|
+ payload = {
|
|
|
+ 'msg_id': f"disc_{int(time.time() * 1000)}",
|
|
|
+ 'timestamp': int(time.time() * 1000),
|
|
|
+ 'type': 'DISCOVER',
|
|
|
+ 'payload': {
|
|
|
+ 'customer_id': customer_id,
|
|
|
+ 'action': 'register'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ success, message = mqtt_client.publish(discover_topic, json.dumps(payload))
|
|
|
+ logger.info(f"发送DTU发现广播: topic={discover_topic}, payload={payload}")
|
|
|
+
|
|
|
+ return jsonify({
|
|
|
+ 'success': success,
|
|
|
+ 'message': 'DTU发现广播已发送' if success else message,
|
|
|
+ 'topic': discover_topic
|
|
|
+ })
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/mqtt/broadcast_config', methods=['POST'])
|
|
|
+def mqtt_broadcast_config():
|
|
|
+ """发送DTU批量配置广播 (7.15)
|
|
|
+
|
|
|
+ 服务器通过广播主题发送CONFIG消息,
|
|
|
+ 用于批量配置所有在线DTU
|
|
|
+ """
|
|
|
+ data = request.json or {}
|
|
|
+ customer_id = data.get('customer_id', dtu_config.get('customer_id', 'default'))
|
|
|
+ config_settings = data.get('config', {})
|
|
|
+
|
|
|
+ config_topic = build_dtu_topic('broadcast', 'dtu', 'config')
|
|
|
+ payload = {
|
|
|
+ 'msg_id': f"cfg_{int(time.time() * 1000)}",
|
|
|
+ 'timestamp': int(time.time() * 1000),
|
|
|
+ 'type': 'CONFIG',
|
|
|
+ 'payload': {
|
|
|
+ 'customer_id': customer_id,
|
|
|
+ 'config': config_settings
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ success, message = mqtt_client.publish(config_topic, json.dumps(payload))
|
|
|
+ logger.info(f"发送DTU批量配置广播: topic={config_topic}, payload={payload}")
|
|
|
+
|
|
|
+ return jsonify({
|
|
|
+ 'success': success,
|
|
|
+ 'message': 'DTU批量配置广播已发送' if success else message,
|
|
|
+ 'topic': config_topic,
|
|
|
+ 'config': config_settings
|
|
|
+ })
|
|
|
+
|
|
|
+
|
|
|
@app.route('/api/data/serial', methods=['GET'])
|
|
|
def get_serial_data():
|
|
|
"""获取串口数据"""
|
|
|
@@ -1708,6 +1996,72 @@ def confirm_devices():
|
|
|
return jsonify({'success': False, 'message': str(e)}), 500
|
|
|
|
|
|
|
|
|
+# 端口状态追踪 - 用于存储每个端口的事件状态
|
|
|
+port_event_history = [] # 存储最近的端口事件
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/port/status', methods=['GET'])
|
|
|
+def get_port_status():
|
|
|
+ """获取所有端口状态"""
|
|
|
+ return jsonify({'success': True, 'port_state': port_state})
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/port/events', methods=['GET'])
|
|
|
+def get_port_events():
|
|
|
+ """获取端口事件历史"""
|
|
|
+ limit = request.args.get('limit', 100, type=int)
|
|
|
+ return jsonify({
|
|
|
+ 'success': True,
|
|
|
+ 'events': port_event_history[-limit:]
|
|
|
+ })
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/port/clear_events', methods=['POST'])
|
|
|
+def clear_port_events():
|
|
|
+ """清除端口事件历史"""
|
|
|
+ global port_event_history
|
|
|
+ port_event_history = []
|
|
|
+ return jsonify({'success': True, 'message': '事件历史已清除'})
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/panel/status', methods=['GET'])
|
|
|
+def get_panel_status():
|
|
|
+ """获取所有面板状态"""
|
|
|
+ # 构建面板状态数据
|
|
|
+ panel_status = {}
|
|
|
+ for panel_id, ports in port_state.items():
|
|
|
+ port_count = len(ports)
|
|
|
+ alarm_count = sum(1 for p in ports.values() if p.get('alarm_count', 0) > 0)
|
|
|
+ connected_count = sum(1 for p in ports.values() if p.get('last_uid'))
|
|
|
+ panel_status[panel_id] = {
|
|
|
+ 'panel_id': panel_id,
|
|
|
+ 'port_count': port_count,
|
|
|
+ 'connected_count': connected_count,
|
|
|
+ 'alarm_count': alarm_count,
|
|
|
+ 'status': 'online' if connected_count > 0 else 'offline'
|
|
|
+ }
|
|
|
+ # 添加面板配置信息
|
|
|
+ for panel_id, cfg in panel_config.items():
|
|
|
+ if panel_id in panel_status:
|
|
|
+ panel_status[panel_id].update({
|
|
|
+ 'address': cfg.get('address'),
|
|
|
+ 'position': cfg.get('position'),
|
|
|
+ 'panel_uid': cfg.get('panel_uid')
|
|
|
+ })
|
|
|
+ else:
|
|
|
+ panel_status[panel_id] = {
|
|
|
+ 'panel_id': panel_id,
|
|
|
+ 'address': cfg.get('address'),
|
|
|
+ 'position': cfg.get('position'),
|
|
|
+ 'panel_uid': cfg.get('panel_uid'),
|
|
|
+ 'port_count': 0,
|
|
|
+ 'connected_count': 0,
|
|
|
+ 'alarm_count': 0,
|
|
|
+ 'status': 'offline'
|
|
|
+ }
|
|
|
+ return jsonify({'success': True, 'panels': panel_status})
|
|
|
+
|
|
|
+
|
|
|
# LED 状态追踪 (内存中跟踪每个灯的最后设置状态)
|
|
|
led_states = {i: 0 for i in range(1, 25)} # 0=off, 1=red, 2=green, 3=blue
|
|
|
|
|
|
@@ -1877,6 +2231,331 @@ def get_dtu_status():
|
|
|
return jsonify({'success': False, 'message': str(e)}), 500
|
|
|
|
|
|
|
|
|
+# OTA状态存储
|
|
|
+ota_status = {
|
|
|
+ 'status': 'IDLE', # IDLE, DOWNLOADING, VERIFYING, FLASHING, SUCCESS, FAILED
|
|
|
+ 'progress': 0,
|
|
|
+ 'firmware_version': None,
|
|
|
+ 'target_version': None,
|
|
|
+ 'error_code': None,
|
|
|
+ 'error_message': None,
|
|
|
+ 'last_update': None
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/dtu/ota_status', methods=['GET'])
|
|
|
+def get_ota_status():
|
|
|
+ """获取OTA升级状态"""
|
|
|
+ try:
|
|
|
+ return jsonify({
|
|
|
+ 'success': True,
|
|
|
+ 'data': {
|
|
|
+ 'current_firmware': dtu_config.get('firmware_version', 'v1.0.0'),
|
|
|
+ 'ota_status': ota_status.get('status', 'IDLE'),
|
|
|
+ 'ota_progress': ota_status.get('progress', 0),
|
|
|
+ 'target_version': ota_status.get('target_version'),
|
|
|
+ 'error_code': ota_status.get('error_code'),
|
|
|
+ 'error_message': ota_status.get('error_message'),
|
|
|
+ 'last_update': ota_status.get('last_update')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"获取OTA状态失败: {str(e)}")
|
|
|
+ return jsonify({'success': False, 'message': str(e)}), 500
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/dtu/ota_upgrade', methods=['POST'])
|
|
|
+def trigger_ota_upgrade():
|
|
|
+ """触发OTA升级"""
|
|
|
+ try:
|
|
|
+ if not mqtt_status:
|
|
|
+ return jsonify({'success': False, 'message': 'MQTT未连接'}), 400
|
|
|
+
|
|
|
+ data = request.json
|
|
|
+ if not data:
|
|
|
+ return jsonify({'success': False, 'message': '请求体不能为空'}), 400
|
|
|
+
|
|
|
+ # 验证必填字段
|
|
|
+ required_fields = ['firmware_url', 'firmware_version', 'file_size', 'checksum', 'checksum_type']
|
|
|
+ missing = [f for f in required_fields if not data.get(f)]
|
|
|
+ if missing:
|
|
|
+ return jsonify({'success': False, 'message': f'缺少必填字段: {", ".join(missing)}'}), 400
|
|
|
+
|
|
|
+ # 构建OTA升级命令
|
|
|
+ msg_id = f"ota_{int(time.time() * 1000)}"
|
|
|
+ control_topic = build_dtu_topic(
|
|
|
+ dtu_config['customer_id'],
|
|
|
+ 'dtu',
|
|
|
+ dtu_config['dtu_id'],
|
|
|
+ 'control'
|
|
|
+ )
|
|
|
+
|
|
|
+ payload = {
|
|
|
+ 'msg_id': msg_id,
|
|
|
+ 'timestamp': int(time.time() * 1000),
|
|
|
+ 'dtu_id': dtu_config['dtu_id'],
|
|
|
+ 'type': 'CONTROL',
|
|
|
+ 'payload': {
|
|
|
+ 'command': 'OTA_UPGRADE',
|
|
|
+ 'target': 'dtu',
|
|
|
+ 'params': {
|
|
|
+ 'firmware_url': data['firmware_url'],
|
|
|
+ 'firmware_version': data['firmware_version'],
|
|
|
+ 'file_size': data['file_size'],
|
|
|
+ 'checksum': data['checksum'],
|
|
|
+ 'checksum_type': data['checksum_type'],
|
|
|
+ 'force_upgrade': data.get('force_upgrade', False)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ # 发布OTA升级命令
|
|
|
+ mqtt_client.publish(control_topic, json.dumps(payload))
|
|
|
+ logger.info(f"OTA升级命令已发送: {data['firmware_version']}")
|
|
|
+
|
|
|
+ # 更新OTA状态
|
|
|
+ ota_status['status'] = 'DOWNLOADING'
|
|
|
+ ota_status['progress'] = 0
|
|
|
+ ota_status['target_version'] = data['firmware_version']
|
|
|
+ ota_status['error_code'] = None
|
|
|
+ ota_status['error_message'] = None
|
|
|
+ ota_status['last_update'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
|
+
|
|
|
+ return jsonify({
|
|
|
+ 'success': True,
|
|
|
+ 'message': 'OTA升级命令已发送',
|
|
|
+ 'data': {
|
|
|
+ 'msg_id': msg_id,
|
|
|
+ 'target_version': data['firmware_version'],
|
|
|
+ 'ota_status': 'DOWNLOADING'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"触发OTA升级失败: {str(e)}")
|
|
|
+ return jsonify({'success': False, 'message': str(e)}), 500
|
|
|
+
|
|
|
+
|
|
|
+# 处理OTA状态上报
|
|
|
+def handle_ota_status(dtu_id, payload):
|
|
|
+ """处理OTA状态上报"""
|
|
|
+ ota_status['status'] = payload.get('ota_status', 'IDLE')
|
|
|
+ ota_status['progress'] = payload.get('ota_progress', 0)
|
|
|
+ ota_status['firmware_version'] = payload.get('firmware_version')
|
|
|
+ ota_status['last_update'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
|
+
|
|
|
+ error_code = payload.get('error_code')
|
|
|
+ if error_code and error_code != 0:
|
|
|
+ ota_status['error_code'] = error_code
|
|
|
+ ota_status['error_message'] = payload.get('error_message', get_ota_error_message(error_code))
|
|
|
+ ota_status['status'] = 'FAILED'
|
|
|
+ elif ota_status['status'] == 'SUCCESS':
|
|
|
+ ota_status['error_code'] = None
|
|
|
+ ota_status['error_message'] = None
|
|
|
+ # 更新DTU配置中的固件版本
|
|
|
+ dtu_config['firmware_version'] = ota_status['firmware_version']
|
|
|
+
|
|
|
+ logger.info(f"OTA状态更新: {ota_status['status']}, 进度: {ota_status['progress']}%")
|
|
|
+
|
|
|
+
|
|
|
+def get_ota_error_message(error_code):
|
|
|
+ """获取OTA错误码对应的错误信息"""
|
|
|
+ error_messages = {
|
|
|
+ 1021: '已是目标版本',
|
|
|
+ 1022: '校验失败',
|
|
|
+ 1023: '下载失败',
|
|
|
+ 1024: '写入失败',
|
|
|
+ 1025: '存储空间不足'
|
|
|
+ }
|
|
|
+ return error_messages.get(error_code, f'未知错误码: {error_code}')
|
|
|
+
|
|
|
+
|
|
|
+# ==================== 环境传感器 API ====================
|
|
|
+
|
|
|
+# 环境传感器数据存储
|
|
|
+env_sensor_data = {
|
|
|
+ 'temperature': None, # 环境温度 (℃)
|
|
|
+ 'humidity': None, # 环境湿度 (%)
|
|
|
+ 'dtu_temperature': None, # DTU主板温度 (℃)
|
|
|
+ 'update_time': None, # 数据更新时间
|
|
|
+ 'sensor_update_time': None, # 传感器更新时间
|
|
|
+ 'connected': False # 传感器连接状态
|
|
|
+}
|
|
|
+
|
|
|
+# 环境传感器告警阈值
|
|
|
+env_sensor_threshold = {
|
|
|
+ 'temp_high': 45.0, # 环境温度上限 (℃)
|
|
|
+ 'temp_low': -10.0, # 环境温度下限 (℃)
|
|
|
+ 'humidity_high': 80.0, # 环境湿度上限 (%)
|
|
|
+ 'humidity_low': 20.0, # 环境湿度下限 (%)
|
|
|
+ 'dtu_temp_high': 70.0 # DTU主板温度上限 (℃)
|
|
|
+}
|
|
|
+
|
|
|
+# 环境传感器历史数据 (保留最近1000条)
|
|
|
+env_sensor_history = []
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/sensor/env', methods=['GET'])
|
|
|
+def get_env_sensor_data():
|
|
|
+ """获取环境传感器当前数据"""
|
|
|
+ return jsonify({
|
|
|
+ 'success': True,
|
|
|
+ 'data': env_sensor_data
|
|
|
+ })
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/sensor/threshold', methods=['GET', 'POST'])
|
|
|
+def handle_env_sensor_threshold():
|
|
|
+ """获取或设置环境传感器告警阈值"""
|
|
|
+ if request.method == 'GET':
|
|
|
+ return jsonify({
|
|
|
+ 'success': True,
|
|
|
+ 'data': env_sensor_threshold
|
|
|
+ })
|
|
|
+ else:
|
|
|
+ try:
|
|
|
+ data = request.get_json()
|
|
|
+ for key in env_sensor_threshold.keys():
|
|
|
+ if key in data:
|
|
|
+ env_sensor_threshold[key] = float(data[key])
|
|
|
+ logger.info(f"更新环境传感器告警阈值: {env_sensor_threshold}")
|
|
|
+ return jsonify({
|
|
|
+ 'success': True,
|
|
|
+ 'message': '阈值设置成功',
|
|
|
+ 'data': env_sensor_threshold
|
|
|
+ })
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"设置告警阈值失败: {str(e)}")
|
|
|
+ return jsonify({'success': False, 'message': str(e)}), 400
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/api/sensor/history', methods=['GET'])
|
|
|
+def get_env_sensor_history():
|
|
|
+ """获取环境传感器历史数据"""
|
|
|
+ try:
|
|
|
+ import datetime
|
|
|
+ # 获取查询参数
|
|
|
+ range_param = request.args.get('range', '24h')
|
|
|
+ start_time = request.args.get('start_time')
|
|
|
+ end_time = request.args.get('end_time')
|
|
|
+ limit = request.args.get('limit', 100, type=int)
|
|
|
+
|
|
|
+ filtered_history = env_sensor_history
|
|
|
+
|
|
|
+ # 解析range参数
|
|
|
+ if not start_time and not end_time:
|
|
|
+ now = datetime.datetime.now()
|
|
|
+ if range_param == '1h':
|
|
|
+ start_time = (now - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
+ elif range_param == '6h':
|
|
|
+ start_time = (now - datetime.timedelta(hours=6)).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
+ elif range_param == '24h':
|
|
|
+ start_time = (now - datetime.timedelta(hours=24)).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
+ elif range_param == '7d':
|
|
|
+ start_time = (now - datetime.timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
+ elif range_param == '30d':
|
|
|
+ start_time = (now - datetime.timedelta(days=30)).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
+
|
|
|
+ # 按时间过滤
|
|
|
+ if start_time:
|
|
|
+ filtered_history = [h for h in filtered_history if h.get('update_time') >= start_time]
|
|
|
+ if end_time:
|
|
|
+ filtered_history = [h for h in filtered_history if h.get('update_time') <= end_time]
|
|
|
+
|
|
|
+ # 按时间排序 (新到旧)
|
|
|
+ filtered_history = sorted(filtered_history, key=lambda x: x.get('update_time', ''), reverse=True)
|
|
|
+
|
|
|
+ # 限制返回数量
|
|
|
+ filtered_history = filtered_history[:limit]
|
|
|
+
|
|
|
+ return jsonify({
|
|
|
+ 'success': True,
|
|
|
+ 'data': filtered_history,
|
|
|
+ 'total': len(filtered_history)
|
|
|
+ })
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"获取历史数据失败: {str(e)}")
|
|
|
+ return jsonify({'success': False, 'message': str(e)}), 500
|
|
|
+
|
|
|
+
|
|
|
+def update_env_sensor_data(temperature, humidity, dtu_temperature, sensor_update_time):
|
|
|
+ """更新环境传感器数据 (由MQTT消息触发)"""
|
|
|
+ import datetime
|
|
|
+ now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
+
|
|
|
+ env_sensor_data['temperature'] = temperature
|
|
|
+ env_sensor_data['humidity'] = humidity
|
|
|
+ env_sensor_data['dtu_temperature'] = dtu_temperature
|
|
|
+ env_sensor_data['sensor_update_time'] = sensor_update_time
|
|
|
+ env_sensor_data['update_time'] = now
|
|
|
+ env_sensor_data['connected'] = True
|
|
|
+
|
|
|
+ # 添加到历史记录
|
|
|
+ history_record = {
|
|
|
+ 'temperature': temperature,
|
|
|
+ 'humidity': humidity,
|
|
|
+ 'dtu_temperature': dtu_temperature,
|
|
|
+ 'sensor_update_time': sensor_update_time,
|
|
|
+ 'update_time': now
|
|
|
+ }
|
|
|
+ env_sensor_history.append(history_record)
|
|
|
+
|
|
|
+ # 保留最近1000条
|
|
|
+ if len(env_sensor_history) > 1000:
|
|
|
+ env_sensor_history[:] = env_sensor_history[-1000:]
|
|
|
+
|
|
|
+ # 检查告警
|
|
|
+ check_env_sensor_alarms(temperature, humidity, dtu_temperature)
|
|
|
+
|
|
|
+ logger.info(f"环境传感器数据更新: 温度={temperature}℃, 湿度={humidity}%, DTU温度={dtu_temperature}℃")
|
|
|
+
|
|
|
+
|
|
|
+def check_env_sensor_alarms(temperature, humidity, dtu_temperature):
|
|
|
+ """检查环境传感器告警"""
|
|
|
+ alarms = []
|
|
|
+
|
|
|
+ if temperature is not None:
|
|
|
+ if temperature > env_sensor_threshold['temp_high']:
|
|
|
+ alarms.append({
|
|
|
+ 'type': 'temperature_high',
|
|
|
+ 'message': f'环境温度过高: {temperature}℃ (阈值: {env_sensor_threshold["temp_high"]}℃)',
|
|
|
+ 'level': 'warning'
|
|
|
+ })
|
|
|
+ elif temperature < env_sensor_threshold['temp_low']:
|
|
|
+ alarms.append({
|
|
|
+ 'type': 'temperature_low',
|
|
|
+ 'message': f'环境温度过低: {temperature}℃ (阈值: {env_sensor_threshold["temp_low"]}℃)',
|
|
|
+ 'level': 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ if humidity is not None:
|
|
|
+ if humidity > env_sensor_threshold['humidity_high']:
|
|
|
+ alarms.append({
|
|
|
+ 'type': 'humidity_high',
|
|
|
+ 'message': f'环境湿度过高: {humidity}% (阈值: {env_sensor_threshold["humidity_high"]}%)',
|
|
|
+ 'level': 'warning'
|
|
|
+ })
|
|
|
+ elif humidity < env_sensor_threshold['humidity_low']:
|
|
|
+ alarms.append({
|
|
|
+ 'type': 'humidity_low',
|
|
|
+ 'message': f'环境湿度过低: {humidity}% (阈值: {env_sensor_threshold["humidity_low"]}%)',
|
|
|
+ 'level': 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ if dtu_temperature is not None:
|
|
|
+ if dtu_temperature > env_sensor_threshold['dtu_temp_high']:
|
|
|
+ alarms.append({
|
|
|
+ 'type': 'dtu_temp_high',
|
|
|
+ 'message': f'DTU主板温度过高: {dtu_temperature}℃ (阈值: {env_sensor_threshold["dtu_temp_high"]}℃)',
|
|
|
+ 'level': 'critical'
|
|
|
+ })
|
|
|
+
|
|
|
+ # 发送告警通知
|
|
|
+ for alarm in alarms:
|
|
|
+ logger.warning(f"环境传感器告警: {alarm['message']}")
|
|
|
+ # 可以通过WebSocket发送告警
|
|
|
+ socketio.emit('env_sensor_alarm', alarm)
|
|
|
+
|
|
|
+
|
|
|
if __name__ == '__main__':
|
|
|
try:
|
|
|
# 启动前的初始化工作
|