536 lines
19 KiB
Python
536 lines
19 KiB
Python
|
|
from flask import Flask, request, jsonify
|
|||
|
|
from flask_cors import CORS
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import threading
|
|||
|
|
import time
|
|||
|
|
from pathlib import Path
|
|||
|
|
from serial_manager import SerialManager
|
|||
|
|
class ConfigService:
|
|||
|
|
"""配置服务类 - 专注于配置管理,通过回调与串口通信"""
|
|||
|
|
|
|||
|
|
def __init__(self, default_config_path=None, host="127.0.0.1",
|
|||
|
|
port=5000, debug=False, logger=None,
|
|||
|
|
serial_command_callback=None):
|
|||
|
|
"""
|
|||
|
|
初始化配置服务
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
default_config_path: 默认配置文件路径
|
|||
|
|
host: 监听地址
|
|||
|
|
port: 监听端口
|
|||
|
|
debug: 调试模式
|
|||
|
|
logger: 日志记录器
|
|||
|
|
serial_command_callback: 串口命令回调函数
|
|||
|
|
"""
|
|||
|
|
self.app = Flask(__name__)
|
|||
|
|
CORS(self.app)
|
|||
|
|
|
|||
|
|
# 服务配置
|
|||
|
|
self.host = host
|
|||
|
|
self.port = port
|
|||
|
|
self.debug = debug
|
|||
|
|
self.logger = logger
|
|||
|
|
self.serial_command_callback = serial_command_callback
|
|||
|
|
|
|||
|
|
# 配置文件管理
|
|||
|
|
self.config_lock = threading.Lock()
|
|||
|
|
service_settings_file = os.path.join(os.path.dirname(__file__), "config_service_settings.json")
|
|||
|
|
self.default_config_path = default_config_path or self._load_service_settings(service_settings_file)
|
|||
|
|
|
|||
|
|
# 服务状态
|
|||
|
|
self._running = False
|
|||
|
|
self._server_thread = None
|
|||
|
|
|
|||
|
|
# 注册路由
|
|||
|
|
self._register_routes()
|
|||
|
|
|
|||
|
|
def _log(self, message):
|
|||
|
|
"""日志输出"""
|
|||
|
|
if self.logger:
|
|||
|
|
self.logger.info(message)
|
|||
|
|
else:
|
|||
|
|
print(f"[ConfigService] {message}")
|
|||
|
|
|
|||
|
|
def _execute_serial_command(self, command_type: str, **kwargs) -> dict[str, any]:
|
|||
|
|
"""
|
|||
|
|
执行串口命令(通过回调)
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
command_type: 命令类型("power_on", "power_off", "read_status")
|
|||
|
|
**kwargs: 命令参数
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
操作结果
|
|||
|
|
"""
|
|||
|
|
if not self.serial_command_callback:
|
|||
|
|
return {"success": False, "error": "串口回调未设置"}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
return self.serial_command_callback(command_type, **kwargs)
|
|||
|
|
except Exception as e:
|
|||
|
|
return {"success": False, "error": f"执行串口命令失败: {str(e)}"}
|
|||
|
|
|
|||
|
|
def _register_routes(self):
|
|||
|
|
"""注册API路由"""
|
|||
|
|
|
|||
|
|
@self.app.route('/api/config', methods=['GET'])
|
|||
|
|
def get_config():
|
|||
|
|
"""
|
|||
|
|
获取配置文件内容
|
|||
|
|
查询参数:
|
|||
|
|
path: 可选,指定配置文件路径
|
|||
|
|
"""
|
|||
|
|
config_path = request.args.get('path', None)
|
|||
|
|
content = self.read_config_file(config_path)
|
|||
|
|
|
|||
|
|
if "error" in content:
|
|||
|
|
return jsonify(content), 500
|
|||
|
|
|
|||
|
|
return jsonify({
|
|||
|
|
"success": True,
|
|||
|
|
"config": content,
|
|||
|
|
"path": config_path or self.default_config_path
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
@self.app.route('/api/config', methods=['POST'])
|
|||
|
|
def update_config():
|
|||
|
|
"""
|
|||
|
|
更新配置文件内容
|
|||
|
|
请求体:
|
|||
|
|
{
|
|||
|
|
"config": {...}, # 配置内容
|
|||
|
|
"path": "..." # 可选,配置文件路径
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
data = request.get_json()
|
|||
|
|
|
|||
|
|
if not data or "config" not in data:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": "请求体必须包含config字段"
|
|||
|
|
}), 400
|
|||
|
|
|
|||
|
|
config_content = data["config"]
|
|||
|
|
config_path = data.get("path", None)
|
|||
|
|
|
|||
|
|
result = self.write_config_file(config_content, config_path)
|
|||
|
|
|
|||
|
|
if result.get("success"):
|
|||
|
|
return jsonify(result)
|
|||
|
|
else:
|
|||
|
|
return jsonify(result), 500
|
|||
|
|
|
|||
|
|
@self.app.route('/api/config/text', methods=['GET'])
|
|||
|
|
def get_config_text():
|
|||
|
|
"""
|
|||
|
|
获取配置文件的纯文本内容(格式化的JSON)
|
|||
|
|
查询参数:
|
|||
|
|
path: 可选,指定配置文件路径
|
|||
|
|
"""
|
|||
|
|
config_path = request.args.get('path', None)
|
|||
|
|
if config_path is None:
|
|||
|
|
config_path = self.default_config_path
|
|||
|
|
|
|||
|
|
with self.config_lock:
|
|||
|
|
try:
|
|||
|
|
if not os.path.exists(config_path):
|
|||
|
|
return jsonify({"error": f"配置文件不存在: {config_path}"}), 404
|
|||
|
|
|
|||
|
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|||
|
|
content = f.read()
|
|||
|
|
|
|||
|
|
return jsonify({
|
|||
|
|
"success": True,
|
|||
|
|
"text": content,
|
|||
|
|
"path": config_path
|
|||
|
|
})
|
|||
|
|
except Exception as e:
|
|||
|
|
return jsonify({"error": f"读取配置文件失败: {str(e)}"}), 500
|
|||
|
|
|
|||
|
|
@self.app.route('/api/config/text', methods=['POST'])
|
|||
|
|
def update_config_text():
|
|||
|
|
"""
|
|||
|
|
以纯文本方式更新配置文件内容
|
|||
|
|
请求体:
|
|||
|
|
{
|
|||
|
|
"text": "...", # 配置文件文本内容
|
|||
|
|
"path": "..." # 可选,配置文件路径
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
data = request.get_json()
|
|||
|
|
|
|||
|
|
if not data or "text" not in data:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": "请求体必须包含text字段"
|
|||
|
|
}), 400
|
|||
|
|
|
|||
|
|
config_text = data["text"]
|
|||
|
|
config_path = data.get("path", self.default_config_path)
|
|||
|
|
|
|||
|
|
with self.config_lock:
|
|||
|
|
try:
|
|||
|
|
# 备份原文件
|
|||
|
|
if os.path.exists(config_path):
|
|||
|
|
backup_path = f"{config_path}.backup"
|
|||
|
|
with open(config_path, 'r', encoding='utf-8') as f_src:
|
|||
|
|
with open(backup_path, 'w', encoding='utf-8') as f_dst:
|
|||
|
|
f_dst.write(f_src.read())
|
|||
|
|
|
|||
|
|
# 写入新配置
|
|||
|
|
with open(config_path, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(config_text)
|
|||
|
|
|
|||
|
|
return jsonify({
|
|||
|
|
"success": True,
|
|||
|
|
"message": "配置文件已更新"
|
|||
|
|
})
|
|||
|
|
except Exception as e:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": f"写入配置文件失败: {str(e)}"
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
@self.app.route('/api/health', methods=['GET'])
|
|||
|
|
def health_check():
|
|||
|
|
"""健康检查接口"""
|
|||
|
|
return jsonify({
|
|||
|
|
"status": "ok",
|
|||
|
|
"service": "config-service",
|
|||
|
|
"version": "1.0.0"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
@self.app.route('/api/power/on', methods=['POST'])
|
|||
|
|
def power_on():
|
|||
|
|
"""实验台上电"""
|
|||
|
|
try:
|
|||
|
|
result = self._execute_serial_command("power_on", unit=2, coil_addr=0x0001)
|
|||
|
|
if result.get("success"):
|
|||
|
|
return jsonify({
|
|||
|
|
"success": True,
|
|||
|
|
"message": "实验台上电成功",
|
|||
|
|
"data": result
|
|||
|
|
})
|
|||
|
|
else:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": result.get("error", "上电操作失败")
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": f"上电操作异常: {str(e)}"
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
@self.app.route('/api/power/off', methods=['POST'])
|
|||
|
|
def power_off():
|
|||
|
|
"""实验台断电"""
|
|||
|
|
try:
|
|||
|
|
result = self._execute_serial_command("power_off", unit=2, coil_addr=0x0001)
|
|||
|
|
|
|||
|
|
if result.get("success"):
|
|||
|
|
return jsonify({
|
|||
|
|
"success": True,
|
|||
|
|
"message": "实验台断电成功",
|
|||
|
|
"data": result
|
|||
|
|
})
|
|||
|
|
else:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": result.get("error", "断电操作失败")
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": f"断电操作异常: {str(e)}"
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
@self.app.route('/api/power/status', methods=['GET'])
|
|||
|
|
def power_status():
|
|||
|
|
"""获取电源状态"""
|
|||
|
|
try:
|
|||
|
|
result = self._execute_serial_command("read_status", unit=2, start_addr=13, quantity=1)
|
|||
|
|
|
|||
|
|
if result.get("success"):
|
|||
|
|
registers = result.get("registers", [])
|
|||
|
|
status_value = registers[0] if registers else 0
|
|||
|
|
remote_close = (status_value == 0xFF00)
|
|||
|
|
|
|||
|
|
return jsonify({
|
|||
|
|
"success": True,
|
|||
|
|
"remote_close": remote_close,
|
|||
|
|
"value": status_value,
|
|||
|
|
"raw_data": result.get("raw_hex", "")
|
|||
|
|
})
|
|||
|
|
else:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": result.get("error", "状态读取失败")
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": f"状态读取异常: {str(e)}"
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
@self.app.route('/api/light/green/on', methods=['POST'])
|
|||
|
|
def green_light_on():
|
|||
|
|
"""开绿灯"""
|
|||
|
|
try:
|
|||
|
|
result = self._execute_serial_command("green_light_on", unit=1, coil_addr=0x0002)
|
|||
|
|
|
|||
|
|
if result.get("success"):
|
|||
|
|
return jsonify({
|
|||
|
|
"success": True,
|
|||
|
|
"message": "绿灯开启成功",
|
|||
|
|
"data": result
|
|||
|
|
})
|
|||
|
|
else:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": result.get("error", "开绿灯操作失败")
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": f"开绿灯操作异常: {str(e)}"
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
@self.app.route('/api/light/green/off', methods=['POST'])
|
|||
|
|
def green_light_off():
|
|||
|
|
"""关绿灯"""
|
|||
|
|
try:
|
|||
|
|
result = self._execute_serial_command("green_light_off", unit=1, coil_addr=0x0002)
|
|||
|
|
|
|||
|
|
if result.get("success"):
|
|||
|
|
return jsonify({
|
|||
|
|
"success": True,
|
|||
|
|
"message": "绿灯关闭成功",
|
|||
|
|
"data": result
|
|||
|
|
})
|
|||
|
|
else:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": result.get("error", "关绿灯操作失败")
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"error": f"关绿灯操作异常: {str(e)}"
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
# 原有的配置文件操作方法保持不变
|
|||
|
|
def read_config_file(self, config_path: str = None) -> dict:
|
|||
|
|
"""
|
|||
|
|
读取配置文件
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
config_path: 配置文件路径,如果为None则使用默认路径
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
配置文件内容(dict)
|
|||
|
|
"""
|
|||
|
|
if config_path is None:
|
|||
|
|
config_path = self.default_config_path
|
|||
|
|
|
|||
|
|
with self.config_lock:
|
|||
|
|
try:
|
|||
|
|
if not os.path.exists(config_path):
|
|||
|
|
return {"error": f"配置文件不存在: {config_path}"}
|
|||
|
|
|
|||
|
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|||
|
|
content = json.load(f)
|
|||
|
|
return content
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
return {"error": f"JSON解析错误: {str(e)}"}
|
|||
|
|
except Exception as e:
|
|||
|
|
return {"error": f"读取配置文件失败: {str(e)}"}
|
|||
|
|
|
|||
|
|
def write_config_file(self, content: dict, config_path: str = None) -> dict:
|
|||
|
|
"""
|
|||
|
|
写入配置文件
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
content: 要写入的配置内容
|
|||
|
|
config_path: 配置文件路径,如果为None则使用默认路径
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
操作结果
|
|||
|
|
"""
|
|||
|
|
if config_path is None:
|
|||
|
|
config_path = self.default_config_path
|
|||
|
|
|
|||
|
|
with self.config_lock:
|
|||
|
|
try:
|
|||
|
|
# 备份原文件
|
|||
|
|
if os.path.exists(config_path):
|
|||
|
|
backup_path = f"{config_path}.backup"
|
|||
|
|
with open(config_path, 'r', encoding='utf-8') as f_src:
|
|||
|
|
with open(backup_path, 'w', encoding='utf-8') as f_dst:
|
|||
|
|
f_dst.write(f_src.read())
|
|||
|
|
|
|||
|
|
# 写入新配置
|
|||
|
|
with open(config_path, 'w', encoding='utf-8') as f:
|
|||
|
|
json.dump(content, f, ensure_ascii=False, indent=2)
|
|||
|
|
|
|||
|
|
return {"success": True, "message": "配置文件已更新"}
|
|||
|
|
except Exception as e:
|
|||
|
|
return {"success": False, "error": f"写入配置文件失败: {str(e)}"}
|
|||
|
|
|
|||
|
|
def start(self):
|
|||
|
|
"""启动配置服务"""
|
|||
|
|
if self._running:
|
|||
|
|
self._log("配置服务已在运行中")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
self._running = True
|
|||
|
|
self._server_thread = threading.Thread(target=self._run_server, daemon=True)
|
|||
|
|
self._server_thread.start()
|
|||
|
|
self._log(f"配置服务已启动,监听 {self.host}:{self.port}")
|
|||
|
|
|
|||
|
|
def _run_server(self):
|
|||
|
|
"""运行Flask服务器"""
|
|||
|
|
self.app.run(host=self.host, port=self.port, debug=self.debug, use_reloader=False)
|
|||
|
|
|
|||
|
|
def stop(self):
|
|||
|
|
"""停止配置服务"""
|
|||
|
|
self._running = False
|
|||
|
|
self._log("配置服务停止请求已发送")
|
|||
|
|
|
|||
|
|
def _load_service_settings(self, settings_path: str) -> str:
|
|||
|
|
"""
|
|||
|
|
加载服务设置文件
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
settings_path: 设置文件路径
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
默认配置文件路径
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
if os.path.exists(settings_path):
|
|||
|
|
with open(settings_path, 'r', encoding='utf-8') as f:
|
|||
|
|
settings = json.load(f)
|
|||
|
|
default_config = settings.get('paths', {}).get('default_config', 'config.json')
|
|||
|
|
# 返回相对于当前文件的路径
|
|||
|
|
base_dir = os.path.dirname(__file__)
|
|||
|
|
return os.path.join(base_dir, default_config)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"加载服务设置失败: {e}")
|
|||
|
|
|
|||
|
|
# 默认返回当前目录下的 config.json
|
|||
|
|
base_dir = os.path.dirname(__file__)
|
|||
|
|
return os.path.join(base_dir, 'config.json')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""主应用程序入口"""
|
|||
|
|
|
|||
|
|
def serial_command_handler(command_type: str, **kwargs) -> dict[str, any]:
|
|||
|
|
"""
|
|||
|
|
串口命令处理器
|
|||
|
|
将ConfigService的命令转换为SerialManager的实际操作
|
|||
|
|
"""
|
|||
|
|
serial_mgr = SerialManager() # 获取单例实例
|
|||
|
|
|
|||
|
|
if command_type == "power_on":
|
|||
|
|
return {
|
|||
|
|
"success": serial_mgr.write_single_coil(
|
|||
|
|
kwargs.get('unit', 2),
|
|||
|
|
kwargs.get('coil_addr', 0x0001),
|
|||
|
|
True
|
|||
|
|
),
|
|||
|
|
"operation": "power_on"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
elif command_type == "power_off":
|
|||
|
|
return {
|
|||
|
|
"success": serial_mgr.write_single_coil(
|
|||
|
|
kwargs.get('unit', 2),
|
|||
|
|
kwargs.get('coil_addr', 0x0001),
|
|||
|
|
False
|
|||
|
|
),
|
|||
|
|
"operation": "power_off"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
elif command_type == "read_status":
|
|||
|
|
result = serial_mgr.read_holding_registers(
|
|||
|
|
kwargs.get('unit', 2),
|
|||
|
|
kwargs.get('start_addr', 13),
|
|||
|
|
kwargs.get('quantity', 1)
|
|||
|
|
)
|
|||
|
|
# 解析寄存器值
|
|||
|
|
if result.get("success") and "raw" in result:
|
|||
|
|
raw_data = result["raw"]
|
|||
|
|
# Modbus响应格式: [unit, function_code, byte_count, data...]
|
|||
|
|
if len(raw_data) >= 5:
|
|||
|
|
byte_count = raw_data[2]
|
|||
|
|
registers = []
|
|||
|
|
for i in range(0, byte_count, 2):
|
|||
|
|
if i + 3 < len(raw_data):
|
|||
|
|
high_byte = raw_data[i + 3]
|
|||
|
|
low_byte = raw_data[i + 4] if i + 4 < len(raw_data) else 0
|
|||
|
|
value = (high_byte << 8) | low_byte
|
|||
|
|
registers.append(value)
|
|||
|
|
result["registers"] = registers
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
elif command_type == "green_light_on":
|
|||
|
|
return {
|
|||
|
|
"success": serial_mgr.write_single_coil(
|
|||
|
|
kwargs.get('unit', 1),
|
|||
|
|
kwargs.get('coil_addr', 0x0002),
|
|||
|
|
True
|
|||
|
|
),
|
|||
|
|
"operation": "green_light_on"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
elif command_type == "green_light_off":
|
|||
|
|
return {
|
|||
|
|
"success": serial_mgr.write_single_coil(
|
|||
|
|
kwargs.get('unit', 1),
|
|||
|
|
kwargs.get('coil_addr', 0x0002),
|
|||
|
|
False
|
|||
|
|
),
|
|||
|
|
"operation": "green_light_off"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
return {"success": False, "error": f"未知命令类型: {command_type}"}
|
|||
|
|
|
|||
|
|
# 创建串口管理器(单例,只会初始化一次)
|
|||
|
|
serial_manager = SerialManager(port="COM1", baudrate=9600) # 根据实际情况修改端口
|
|||
|
|
|
|||
|
|
# 创建配置服务,传入串口命令回调
|
|||
|
|
config_service = ConfigService(
|
|||
|
|
host="127.0.0.1",
|
|||
|
|
port=5000,
|
|||
|
|
debug=False,
|
|||
|
|
serial_command_callback=serial_command_handler
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
print("🚀 启动配置服务和串口管理...")
|
|||
|
|
print(f"串口状态: {serial_manager.get_status()}")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
config_service.start()
|
|||
|
|
|
|||
|
|
# 保持主线程运行
|
|||
|
|
while config_service._running:
|
|||
|
|
time.sleep(1)
|
|||
|
|
|
|||
|
|
except KeyboardInterrupt:
|
|||
|
|
print("\n🛑 接收到中断信号,正在关闭服务...")
|
|||
|
|
finally:
|
|||
|
|
config_service.stop()
|
|||
|
|
serial_manager.close()
|
|||
|
|
print("✅ 服务已安全退出")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|