将读写功能修改为read_genetic,write_genetic并且添加.exe文件
This commit is contained in:
@ -8,11 +8,11 @@ import logging
|
||||
|
||||
class APIServer:
|
||||
"""REST API服务器,提供PLC数据访问和配置管理功能"""
|
||||
|
||||
def __init__(self, cache_manager, config_path="config/config.json"):
|
||||
|
||||
def __init__(self, cache_manager, config_path="./config/config.json"):
|
||||
"""
|
||||
初始化API服务器
|
||||
|
||||
|
||||
Args:
|
||||
cache_manager: 缓存管理器实例
|
||||
config_path: 配置文件路径
|
||||
@ -25,10 +25,10 @@ class APIServer:
|
||||
self.username = "admin"
|
||||
self.password = "admin123" # 实际应用中应从安全存储获取
|
||||
self.start_time = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
# 在初始化方法中调用 setup_routes
|
||||
self.setup_routes()
|
||||
|
||||
|
||||
def check_auth(self, username, password):
|
||||
"""验证用户名和密码"""
|
||||
return username == self.username and password == self.password
|
||||
@ -36,8 +36,8 @@ class APIServer:
|
||||
def authenticate(self):
|
||||
"""发送401响应要求认证"""
|
||||
return Response(
|
||||
"Unauthorized",
|
||||
401,
|
||||
"Unauthorized",
|
||||
401,
|
||||
{"WWW-Authenticate": 'Basic realm="PLC Gateway Configuration"'}
|
||||
)
|
||||
|
||||
@ -47,7 +47,7 @@ class APIServer:
|
||||
def decorated(*args, **kwargs):
|
||||
if not self.auth_enabled:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
|
||||
auth = request.authorization
|
||||
if not auth or not self.check_auth(auth.username, auth.password):
|
||||
return self.authenticate()
|
||||
@ -62,7 +62,7 @@ class APIServer:
|
||||
for area_name, area in areas.items():
|
||||
last_update = self.cache_manager.last_update[plc_name][area_name]
|
||||
plc_status = self.cache_manager.plc_connection_status.get(plc_name, "unknown")
|
||||
|
||||
|
||||
summary[plc_name][area_name] = {
|
||||
"status": area["status"],
|
||||
"plc_connection_status": plc_status,
|
||||
@ -74,7 +74,7 @@ class APIServer:
|
||||
|
||||
def setup_routes(self):
|
||||
"""设置所有API路由"""
|
||||
|
||||
|
||||
# ===========================
|
||||
# 主页面 - 状态摘要
|
||||
# ===========================
|
||||
@ -108,7 +108,7 @@ class APIServer:
|
||||
<h1>PLC Gateway Status</h1>
|
||||
<p>Gateway running since: {{ start_time }}</p>
|
||||
"""
|
||||
|
||||
|
||||
for plc_name, areas in summary.items():
|
||||
plc_status = self.cache_manager.plc_connection_status.get(plc_name, "unknown")
|
||||
plc_class = ""
|
||||
@ -118,7 +118,7 @@ class APIServer:
|
||||
plc_class = "plc-disconnected"
|
||||
else:
|
||||
plc_class = "plc-never-connected"
|
||||
|
||||
|
||||
html += f'<h2 class="{plc_class}">PLC: {plc_name} (Status: {plc_status})</h2>'
|
||||
html += """
|
||||
<table>
|
||||
@ -131,11 +131,11 @@ class APIServer:
|
||||
<th>Last Update</th>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
|
||||
for area_name, area in areas.items():
|
||||
status_class = ""
|
||||
status_text = area["status"]
|
||||
|
||||
|
||||
if area["status"] == "connected":
|
||||
status_class = "status-connected"
|
||||
elif area["status"] == "never_connected":
|
||||
@ -146,7 +146,7 @@ class APIServer:
|
||||
status_text = "Disconnected"
|
||||
else:
|
||||
status_class = "status-disconnected"
|
||||
|
||||
|
||||
html += f"""
|
||||
<tr>
|
||||
<td>{area_name}</td>
|
||||
@ -157,9 +157,9 @@ class APIServer:
|
||||
<td>{area['last_update']}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
|
||||
html += "</table>"
|
||||
|
||||
|
||||
# 添加API文档部分
|
||||
html += """
|
||||
<div class="api-section">
|
||||
@ -219,7 +219,7 @@ class APIServer:
|
||||
<p>PLC Gateway v1.0 | <a href="/api/status">System Status</a></p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
html += """
|
||||
</body>
|
||||
</html>
|
||||
@ -236,10 +236,10 @@ class APIServer:
|
||||
for plc_name in self.cache_manager.plc_connection_status:
|
||||
plc_statuses[plc_name] = {
|
||||
"status": self.cache_manager.plc_connection_status[plc_name],
|
||||
"last_connected": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.cache_manager.plc_last_connected[plc_name]))
|
||||
"last_connected": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.cache_manager.plc_last_connected[plc_name]))
|
||||
if self.cache_manager.plc_last_connected[plc_name] > 0 else "Never"
|
||||
}
|
||||
|
||||
|
||||
return jsonify({
|
||||
"status": "running",
|
||||
"start_time": self.start_time,
|
||||
@ -257,7 +257,7 @@ class APIServer:
|
||||
"""配置编辑页面"""
|
||||
config = self.config_manager.get_config()
|
||||
config_json = json.dumps(config, indent=2)
|
||||
|
||||
|
||||
html = """
|
||||
<html>
|
||||
<head>
|
||||
@ -469,7 +469,7 @@ class APIServer:
|
||||
</html>
|
||||
"""
|
||||
return render_template_string(
|
||||
html,
|
||||
html,
|
||||
config_json=config_json,
|
||||
username=self.username,
|
||||
password=self.password
|
||||
@ -505,7 +505,7 @@ class APIServer:
|
||||
try:
|
||||
config = request.json
|
||||
reload = request.args.get('reload', 'false').lower() == 'true'
|
||||
|
||||
|
||||
success, message = self.config_manager.save_config(config)
|
||||
if success:
|
||||
if reload:
|
||||
@ -1277,13 +1277,13 @@ class APIServer:
|
||||
if not data:
|
||||
# 如果没有提供数据,返回错误
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"status": "error",
|
||||
"message": "No data provided",
|
||||
"plc_connection_status": self.cache_manager.plc_connection_status.get(plc_name, "unknown"),
|
||||
"last_update": 0,
|
||||
"last_update_formatted": "N/A"
|
||||
}), 400
|
||||
|
||||
|
||||
success, error, plc_status, update_time = self.cache_manager.write_area(plc_name, area_name, offset, data)
|
||||
if error:
|
||||
return jsonify({
|
||||
@ -1397,7 +1397,7 @@ class APIServer:
|
||||
"last_update": 0,
|
||||
"last_update_formatted": "N/A"
|
||||
}), 400
|
||||
|
||||
|
||||
requests = request.json
|
||||
if not isinstance(requests, list):
|
||||
return jsonify({
|
||||
@ -1407,9 +1407,9 @@ class APIServer:
|
||||
"last_update": 0,
|
||||
"last_update_formatted": "N/A"
|
||||
}), 400
|
||||
|
||||
|
||||
self.logger.info(f"Received batch write request: {json.dumps(requests)}")
|
||||
|
||||
|
||||
results = self.cache_manager.batch_write(requests)
|
||||
return jsonify(results)
|
||||
except Exception as e:
|
||||
@ -1422,6 +1422,119 @@ class APIServer:
|
||||
"last_update_formatted": "N/A"
|
||||
}), 500
|
||||
|
||||
# 通用读取接口
|
||||
@self.app.route("/api/read_generic/<plc_name>/<area_name>/<int:offset>/<data_type>", methods=["GET"],
|
||||
endpoint="read_generic")
|
||||
def read_generic(plc_name, area_name, offset, data_type):
|
||||
"""通用读取接口"""
|
||||
print("Enter Read generic")
|
||||
# 检查请求参数
|
||||
count = request.args.get('count', 1, type=int)
|
||||
if count < 1:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Count must be at least 1",
|
||||
"plc_name": plc_name,
|
||||
"area_name": area_name
|
||||
}), 400
|
||||
|
||||
# 执行读取
|
||||
result, error, plc_status, update_time = self.cache_manager.read_generic(
|
||||
plc_name,
|
||||
area_name,
|
||||
offset,
|
||||
data_type,
|
||||
count
|
||||
)
|
||||
|
||||
if error:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"plc_name": plc_name,
|
||||
"area_name": area_name,
|
||||
"message": error,
|
||||
"plc_connection_status": plc_status,
|
||||
"last_update": update_time,
|
||||
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(
|
||||
update_time)) if update_time > 0 else "Never"
|
||||
}), 400
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"plc_name": plc_name,
|
||||
"area_name": area_name,
|
||||
"offset": offset,
|
||||
"data_type": data_type,
|
||||
"count": count,
|
||||
"data": result,
|
||||
"plc_connection_status": plc_status,
|
||||
"last_update": update_time,
|
||||
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time))
|
||||
})
|
||||
|
||||
# 通用写入接口
|
||||
@self.app.route("/api/write_generic/<plc_name>/<area_name>/<int:offset>/<data_type>", methods=["POST"],
|
||||
endpoint="write_generic")
|
||||
def write_generic(plc_name, area_name, offset, data_type):
|
||||
"""通用写入接口"""
|
||||
# 检查请求数据
|
||||
if not request.is_json:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Request must be JSON (Content-Type: application/json)",
|
||||
"plc_name": plc_name,
|
||||
"area_name": area_name
|
||||
}), 400
|
||||
json_data = request.get_json()
|
||||
if "value" not in json_data and "values" not in json_data:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Missing 'value' or 'values' field",
|
||||
"plc_name": plc_name,
|
||||
"area_name": area_name
|
||||
}), 400
|
||||
|
||||
# 确定要写入的值
|
||||
value = json_data.get("value", json_data.get("values"))
|
||||
|
||||
# 执行写入
|
||||
success, error, plc_status, update_time = self.cache_manager.write_generic(
|
||||
plc_name,
|
||||
area_name,
|
||||
offset,
|
||||
data_type,
|
||||
value
|
||||
)
|
||||
|
||||
if error:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"plc_name": plc_name,
|
||||
"area_name": area_name,
|
||||
"message": error,
|
||||
"plc_connection_status": plc_status,
|
||||
"last_update": update_time,
|
||||
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(
|
||||
update_time)) if update_time > 0 else "Never"
|
||||
}), 400
|
||||
|
||||
# 确定写入数量
|
||||
count = 1
|
||||
if isinstance(value, list):
|
||||
count = len(value)
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"plc_name": plc_name,
|
||||
"area_name": area_name,
|
||||
"offset": offset,
|
||||
"data_type": data_type,
|
||||
"count": count,
|
||||
"plc_connection_status": plc_status,
|
||||
"last_update": update_time,
|
||||
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time))
|
||||
})
|
||||
|
||||
@self.app.route("/api/batch_write_bool", methods=["POST"], endpoint="batch_write_bool")
|
||||
def batch_write_bool():
|
||||
"""批量写入多个区域的数据"""
|
||||
@ -1475,10 +1588,10 @@ class APIServer:
|
||||
def start(self):
|
||||
"""启动API服务器"""
|
||||
self.server_thread = threading.Thread(
|
||||
target=self.app.run,
|
||||
target=self.app.run,
|
||||
kwargs={
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"threaded": True,
|
||||
"use_reloader": False # 避免在生产环境中使用重载器
|
||||
},
|
||||
|
||||
@ -2,14 +2,15 @@ import threading
|
||||
import time
|
||||
import logging
|
||||
from snap7.util import *
|
||||
import struct
|
||||
|
||||
class CacheManager:
|
||||
"""PLC数据缓存管理器"""
|
||||
|
||||
|
||||
def __init__(self, config, plc_manager, app=None):
|
||||
"""
|
||||
初始化缓存管理器
|
||||
|
||||
|
||||
Args:
|
||||
config: 配置对象
|
||||
plc_manager: PLC管理器实例
|
||||
@ -28,7 +29,7 @@ class CacheManager:
|
||||
self.plc_connection_status = {} # PLC连接状态
|
||||
self.logger = logging.getLogger("CacheManager")
|
||||
self.init_cache()
|
||||
|
||||
|
||||
def init_cache(self):
|
||||
"""初始化缓存结构"""
|
||||
for plc in self.config["plcs"]:
|
||||
@ -37,7 +38,7 @@ class CacheManager:
|
||||
self.last_update[plc_name] = {}
|
||||
self.plc_last_connected[plc_name] = 0 # 初始化为0(未连接)
|
||||
self.plc_connection_status[plc_name] = "never_connected"
|
||||
|
||||
|
||||
for area in plc["areas"]:
|
||||
name = area["name"]
|
||||
# 确保初始状态为断开
|
||||
@ -51,7 +52,7 @@ class CacheManager:
|
||||
"status": "disconnected" # 初始状态为断开
|
||||
}
|
||||
self.last_update[plc_name][name] = 0
|
||||
|
||||
|
||||
def refresh_cache(self):
|
||||
"""后台线程:定期刷新缓存"""
|
||||
while self.running:
|
||||
@ -62,10 +63,10 @@ class CacheManager:
|
||||
plc_name = plc["name"]
|
||||
refresh_interval = plc.get("refresh_interval", 0.5)
|
||||
client = self.plc_manager.get_plc(plc_name)
|
||||
|
||||
|
||||
# 检查PLC连接状态
|
||||
plc_connected = client.connected
|
||||
|
||||
|
||||
# 更新PLC连接状态
|
||||
with self.lock:
|
||||
if plc_connected:
|
||||
@ -76,14 +77,14 @@ class CacheManager:
|
||||
self.plc_connection_status[plc_name] = "never_connected"
|
||||
else:
|
||||
self.plc_connection_status[plc_name] = "disconnected"
|
||||
|
||||
|
||||
# 刷新所有可读区域
|
||||
for area in plc["areas"]:
|
||||
if area["type"] in ["read", "read_write"]:
|
||||
name = area["name"]
|
||||
try:
|
||||
data = client.read_db(area["db_number"], area["offset"], area["size"])
|
||||
|
||||
|
||||
# 更新区域状态基于PLC连接状态和读取结果
|
||||
with self.lock:
|
||||
if plc_connected and data and len(data) == area["size"]:
|
||||
@ -106,27 +107,23 @@ class CacheManager:
|
||||
#计算需要睡眠的时间,确保总等于refresh_time
|
||||
sleep_time = max(0, refresh_interval - execution_time)
|
||||
time.sleep(sleep_time)
|
||||
print(f"plc_name: {plc_name},"
|
||||
f"Cache refresh completed.Execution time: {execution_time:.3f}s,"
|
||||
f"Sleep time: {sleep_time:.3f}s,"
|
||||
f"Total interval: {execution_time + sleep_time:.3f}s")
|
||||
|
||||
# 记录实际刷新间隔
|
||||
self.logger.debug(f"plc_name: {plc_name},"
|
||||
f"Cache refresh completed.Execution time: {execution_time:.3f}s,"
|
||||
f"Sleep time: {sleep_time:.3f}s,"
|
||||
f"Total interval: {execution_time + sleep_time:.3f}s")
|
||||
|
||||
|
||||
time.sleep(self.refresh_interval)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in refresh_cache: {e}")
|
||||
time.sleep(self.refresh_interval)
|
||||
|
||||
|
||||
def start(self):
|
||||
"""启动缓存刷新线程"""
|
||||
if self.running:
|
||||
return
|
||||
|
||||
|
||||
self.running = True
|
||||
self.thread = threading.Thread(
|
||||
target=self.refresh_cache,
|
||||
@ -135,12 +132,12 @@ class CacheManager:
|
||||
)
|
||||
self.thread.start()
|
||||
self.logger.info("Cache manager started")
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""停止缓存刷新线程"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
|
||||
self.running = False
|
||||
if self.thread:
|
||||
# 等待线程结束,但设置超时防止卡死
|
||||
@ -149,17 +146,17 @@ class CacheManager:
|
||||
self.logger.warning("Cache refresh thread did not terminate gracefully")
|
||||
self.thread = None
|
||||
self.logger.info("Cache manager stopped")
|
||||
|
||||
|
||||
def get_plc_connection_status(self, plc_name):
|
||||
"""获取PLC连接状态"""
|
||||
with self.lock:
|
||||
return self.plc_connection_status.get(plc_name, "unknown")
|
||||
|
||||
|
||||
def get_last_update_time(self, plc_name, area_name):
|
||||
"""获取区域数据最后更新时间"""
|
||||
with self.lock:
|
||||
return self.last_update.get(plc_name, {}).get(area_name, 0)
|
||||
|
||||
|
||||
def get_summary(self):
|
||||
"""获取缓存摘要信息"""
|
||||
summary = {}
|
||||
@ -169,14 +166,14 @@ class CacheManager:
|
||||
for area_name, area in areas.items():
|
||||
last_update = self.last_update[plc_name][area_name]
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
|
||||
|
||||
# 区域状态应与PLC连接状态一致,除非有有效数据
|
||||
area_status = area["status"]
|
||||
if plc_status == "never_connected":
|
||||
area_status = "never_connected"
|
||||
elif plc_status == "disconnected" and self.last_update[plc_name][area_name] == 0:
|
||||
area_status = "disconnected"
|
||||
|
||||
|
||||
summary[plc_name][area_name] = {
|
||||
"status": area_status,
|
||||
"plc_connection_status": plc_status,
|
||||
@ -185,24 +182,24 @@ class CacheManager:
|
||||
"type": area["type"]
|
||||
}
|
||||
return summary
|
||||
|
||||
|
||||
def get_area_status(self, plc_name, area_name):
|
||||
"""获取区域状态"""
|
||||
with self.lock:
|
||||
area = self.cache.get(plc_name, {}).get(area_name)
|
||||
if not area:
|
||||
return {"status": "not_found", "message": "PLC or area not found"}
|
||||
|
||||
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
last_update = self.last_update.get(plc_name, {}).get(area_name, 0)
|
||||
|
||||
|
||||
# 区域状态应与PLC连接状态一致,除非有有效数据
|
||||
area_status = area["status"]
|
||||
if plc_status == "never_connected":
|
||||
area_status = "never_connected"
|
||||
elif plc_status == "disconnected" and last_update == 0:
|
||||
area_status = "disconnected"
|
||||
|
||||
|
||||
return {
|
||||
"status": area_status,
|
||||
"plc_connection_status": plc_status,
|
||||
@ -211,24 +208,25 @@ class CacheManager:
|
||||
"size": area["size"],
|
||||
"type": area["type"]
|
||||
}
|
||||
|
||||
|
||||
def read_area(self, plc_name, area_name, offset, length):
|
||||
"""单个区域读取"""
|
||||
with self.lock:
|
||||
area = self.cache.get(plc_name, {}).get(area_name)
|
||||
print("read area :",area)
|
||||
if not area:
|
||||
return None, "Area not found", "unknown", 0
|
||||
|
||||
if offset + length > area["size"]:
|
||||
return None, "Offset out of bounds", "unknown", 0
|
||||
|
||||
|
||||
client = self.plc_manager.get_plc(plc_name)
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
|
||||
|
||||
# 如果PLC未连接,直接返回错误
|
||||
if plc_status != "connected":
|
||||
return None, f"PLC not connected (status: {plc_status})", plc_status, 0
|
||||
|
||||
|
||||
try:
|
||||
data = client.read_db(area["db_number"], area["offset"] + offset, length)
|
||||
# 验证数据有效性
|
||||
@ -239,7 +237,7 @@ class CacheManager:
|
||||
update_time = time.time()
|
||||
self.last_update[plc_name][area_name] = update_time
|
||||
area["status"] = "connected"
|
||||
|
||||
|
||||
return data, None, plc_status, update_time
|
||||
else:
|
||||
area["status"] = plc_status
|
||||
@ -287,29 +285,29 @@ class CacheManager:
|
||||
area["status"] = plc_status
|
||||
self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}")
|
||||
return None, f"Read failed: {str(e)}", plc_status, 0
|
||||
|
||||
|
||||
def write_area(self, plc_name, area_name, offset, data):
|
||||
"""单个区域写入"""
|
||||
with self.lock:
|
||||
area = self.cache.get(plc_name, {}).get(area_name)
|
||||
if not area:
|
||||
return False, "Area not found", "unknown", 0
|
||||
|
||||
|
||||
if area["type"] not in ["write", "read_write"]:
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
return False, "Area is read-only", plc_status, 0
|
||||
|
||||
|
||||
if offset + len(data) > area["size"]:
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
return False, "Offset out of bounds", plc_status, 0
|
||||
|
||||
|
||||
client = self.plc_manager.get_plc(plc_name)
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
|
||||
|
||||
# 如果PLC未连接,直接返回错误
|
||||
if plc_status != "connected":
|
||||
return False, f"PLC not connected (status: {plc_status})", plc_status, 0
|
||||
|
||||
|
||||
try:
|
||||
success = client.write_db(area["db_number"], area["offset"] + offset, data)
|
||||
if success:
|
||||
@ -319,7 +317,7 @@ class CacheManager:
|
||||
update_time = time.time()
|
||||
self.last_update[plc_name][area_name] = update_time
|
||||
area["status"] = "connected (last write)"
|
||||
|
||||
|
||||
return True, None, plc_status, update_time
|
||||
else:
|
||||
area["status"] = plc_status
|
||||
@ -464,7 +462,7 @@ class CacheManager:
|
||||
area["status"] = plc_status
|
||||
self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}")
|
||||
return False, f"Write failed: {str(e)}", plc_status, 0
|
||||
|
||||
|
||||
def batch_read(self, requests):
|
||||
"""批量读取"""
|
||||
results = []
|
||||
@ -633,26 +631,234 @@ class CacheManager:
|
||||
"length": len(data)
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def read_generic(self, plc_name, area_name, offset, data_type, count=1):
|
||||
"""通用读取接口"""
|
||||
with self.lock:
|
||||
area = self.cache.get(plc_name, {}).get(area_name)
|
||||
print("area:",area)
|
||||
if not area:
|
||||
return None, "Area not found", "unknown", 0
|
||||
|
||||
if area["type"] not in ["read", "read_write"]:
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
return None, "Area is read-only", plc_status, 0
|
||||
|
||||
# 计算实际DB偏移
|
||||
db_offset = area["offset"] + offset
|
||||
|
||||
# 确保在区域内
|
||||
if data_type == 'bool':
|
||||
required_size = (offset + count + 7) // 8
|
||||
elif data_type in ['int', 'word']:
|
||||
required_size = 2 * count
|
||||
elif data_type in ['dint', 'dword', 'real']:
|
||||
required_size = 4 * count
|
||||
else: # byte
|
||||
required_size = count
|
||||
|
||||
if db_offset + required_size > area["size"] or db_offset < 0:
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
return None, "Offset out of bounds", plc_status, 0
|
||||
|
||||
client = self.plc_manager.get_plc(plc_name)
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
|
||||
# 如果PLC未连接,直接返回错误
|
||||
if plc_status != "connected":
|
||||
return None, f"PLC not connected (status: {plc_status})", plc_status, 0
|
||||
|
||||
try:
|
||||
# 使用Snap7Client的read_generic方法
|
||||
result = client.read_generic(area["db_number"], db_offset, data_type, count)
|
||||
if result is None:
|
||||
return None, "Read failed", plc_status, 0
|
||||
|
||||
# 对于bool类型,需要特殊处理缓存
|
||||
if data_type == 'bool':
|
||||
for i in range(count):
|
||||
byte_offset = offset // 8 + i // 8
|
||||
bit_offset = (offset % 8) + (i % 8)
|
||||
if bit_offset >= 8:
|
||||
byte_offset += 1
|
||||
bit_offset -= 8
|
||||
|
||||
# 读取当前字节值
|
||||
current_byte = area["data"][byte_offset]
|
||||
if result[i]:
|
||||
# 设置位为1
|
||||
new_byte = current_byte | (1 << bit_offset)
|
||||
else:
|
||||
# 设置位为0
|
||||
new_byte = current_byte & ~(1 << bit_offset)
|
||||
area["data"][byte_offset] = new_byte
|
||||
else:
|
||||
# 对于其他类型,直接更新缓存
|
||||
if not isinstance(result, list):
|
||||
result = [result]
|
||||
|
||||
if data_type == 'byte':
|
||||
item_size = 1
|
||||
elif data_type in ['int', 'word']:
|
||||
item_size = 2
|
||||
else: # dint, dword, real
|
||||
item_size = 4
|
||||
|
||||
for i, val in enumerate(result):
|
||||
item_offset = offset + i * item_size
|
||||
if data_type == 'byte':
|
||||
area["data"][item_offset] = val & 0xFF
|
||||
elif data_type in ['int', 'word']:
|
||||
# 2字节数据
|
||||
packed = struct.pack(">h" if data_type == "int" else ">H", val)
|
||||
for j in range(2):
|
||||
area["data"][item_offset + j] = packed[j]
|
||||
elif data_type in ['dint', 'dword', 'real']:
|
||||
# 4字节数据
|
||||
packed = struct.pack(
|
||||
">l" if data_type == "dint" else
|
||||
">I" if data_type == "dword" else
|
||||
">f",
|
||||
val
|
||||
)
|
||||
for j in range(4):
|
||||
area["data"][item_offset + j] = packed[j]
|
||||
|
||||
update_time = time.time()
|
||||
self.last_update[plc_name][area_name] = update_time
|
||||
area["status"] = "connected"
|
||||
return result, None, plc_status, update_time
|
||||
except Exception as e:
|
||||
area["status"] = plc_status
|
||||
self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}")
|
||||
return None, f"Read failed: {str(e)}", plc_status, 0
|
||||
|
||||
def write_generic(self, plc_name, area_name, offset, data_type, value):
|
||||
"""通用写入接口"""
|
||||
with self.lock:
|
||||
area = self.cache.get(plc_name, {}).get(area_name)
|
||||
if not area:
|
||||
return False, "Area not found", "unknown", 0
|
||||
|
||||
if area["type"] not in ["write", "read_write"]:
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
return False, "Area is read-only", plc_status, 0
|
||||
|
||||
# 计算实际DB偏移
|
||||
db_offset = area["offset"] + offset
|
||||
|
||||
# 确保在区域内
|
||||
if data_type == 'bool':
|
||||
# 确定存储这些布尔值需要多少字节。
|
||||
required_size = (offset + (len(value) if isinstance(value, list) else 1) + 7) // 8
|
||||
elif data_type in ['int', 'word']:
|
||||
required_size = 2 * (len(value) if isinstance(value, list) else 1)
|
||||
elif data_type in ['dint', 'dword', 'real']:
|
||||
required_size = 4 * (len(value) if isinstance(value, list) else 1)
|
||||
else: # byte
|
||||
required_size = len(value) if isinstance(value, list) else 1
|
||||
|
||||
if db_offset + required_size > area["size"] or db_offset < 0:
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
return False, "Offset out of bounds", plc_status, 0
|
||||
|
||||
client = self.plc_manager.get_plc(plc_name)
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
|
||||
# 如果PLC未连接,直接返回错误
|
||||
if plc_status != "connected":
|
||||
return False, f"PLC not connected (status: {plc_status})", plc_status, 0
|
||||
|
||||
try:
|
||||
# 使用Snap7Client的write_generic方法
|
||||
success = client.write_generic(area["db_number"], db_offset, data_type, value)
|
||||
if success:
|
||||
# 根据数据类型更新缓存
|
||||
if data_type == 'bool':
|
||||
# 处理bool写入
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
for i, val in enumerate(value):
|
||||
byte_offset = offset // 8 + i // 8
|
||||
bit_offset = (offset % 8) + (i % 8)
|
||||
if bit_offset >= 8:
|
||||
byte_offset += 1
|
||||
bit_offset -= 8
|
||||
|
||||
# 读取当前字节值
|
||||
current_byte = area["data"][byte_offset]
|
||||
if val:
|
||||
# 设置位为1
|
||||
new_byte = current_byte | (1 << bit_offset)
|
||||
else:
|
||||
# 设置位为0
|
||||
new_byte = current_byte & ~(1 << bit_offset)
|
||||
area["data"][byte_offset] = new_byte
|
||||
elif data_type == 'byte':
|
||||
# 处理byte写入
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
for i, val in enumerate(value):
|
||||
area["data"][offset + i] = val & 0xFF
|
||||
elif data_type in ['int', 'word']:
|
||||
# 处理int/word写入
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
for i, val in enumerate(value):
|
||||
# 2字节数据
|
||||
packed = struct.pack(">h" if data_type == "int" else ">H", val)
|
||||
for j in range(2):
|
||||
area["data"][offset + i * 2 + j] = packed[j]
|
||||
elif data_type in ['dint', 'dword', 'real']:
|
||||
# 处理dint/dword/real写入
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
for i, val in enumerate(value):
|
||||
# 4字节数据
|
||||
packed = struct.pack(
|
||||
">l" if data_type == "dint" else
|
||||
">I" if data_type == "dword" else
|
||||
">f",
|
||||
val
|
||||
)
|
||||
for j in range(4):
|
||||
area["data"][offset + i * 4 + j] = packed[j]
|
||||
|
||||
update_time = time.time()
|
||||
self.last_update[plc_name][area_name] = update_time
|
||||
area["status"] = "connected (last write)"
|
||||
return True, None, plc_status, update_time
|
||||
else:
|
||||
area["status"] = plc_status
|
||||
return False, "Write failed", plc_status, 0
|
||||
except Exception as e:
|
||||
area["status"] = plc_status
|
||||
self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}")
|
||||
return False, f"Write failed: {str(e)}", plc_status, 0
|
||||
|
||||
def get_parsed_data(self, plc_name, area_name):
|
||||
"""获取解析后的数据"""
|
||||
from data_parser import parse_data
|
||||
|
||||
|
||||
with self.lock:
|
||||
area = self.cache.get(plc_name, {}).get(area_name)
|
||||
if not area:
|
||||
return {"error": "Area not found"}
|
||||
|
||||
|
||||
plc_status = self.plc_connection_status.get(plc_name, "unknown")
|
||||
last_update = self.last_update.get(plc_name, {}).get(area_name, 0)
|
||||
|
||||
|
||||
# 区域状态应与PLC连接状态一致,除非有有效数据
|
||||
area_status = area["status"]
|
||||
if plc_status == "never_connected":
|
||||
area_status = "never_connected"
|
||||
elif plc_status == "disconnected" and last_update == 0:
|
||||
area_status = "disconnected"
|
||||
|
||||
|
||||
structure = area.get("structure", [])
|
||||
if structure:
|
||||
parsed = parse_data(area["data"], structure)
|
||||
|
||||
@ -4,7 +4,7 @@ import threading
|
||||
from config_loader import load_config
|
||||
from plc_manager import PLCManager
|
||||
from cache_manager import CacheManager
|
||||
from api_server_html import APIServer
|
||||
from api_server import APIServer
|
||||
from config_manager import ConfigManager
|
||||
|
||||
class GatewayApp:
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='main',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='main',
|
||||
)
|
||||
@ -7,11 +7,11 @@ import ast
|
||||
|
||||
class Snap7Client:
|
||||
"""Snap7客户端,处理与PLC的通信"""
|
||||
|
||||
|
||||
def __init__(self, ip, rack, slot, max_retries=5, retry_base_delay=1):
|
||||
"""
|
||||
初始化Snap7客户端
|
||||
|
||||
|
||||
Args:
|
||||
ip: PLC IP地址
|
||||
rack: Rack编号
|
||||
@ -77,19 +77,19 @@ class Snap7Client:
|
||||
def read_db(self, db_number, offset, size):
|
||||
"""
|
||||
从DB块读取数据
|
||||
|
||||
|
||||
Args:
|
||||
db_number: DB编号
|
||||
offset: 起始偏移量
|
||||
size: 读取字节数
|
||||
|
||||
|
||||
Returns:
|
||||
bytearray: 读取的数据,如果失败返回None
|
||||
"""
|
||||
if not self.connected and not self.connect():
|
||||
self.logger.warning(f"Read failed: not connected to {self.ip}")
|
||||
return None # 返回None而不是零填充数据
|
||||
|
||||
|
||||
try:
|
||||
with self.lock: # 进入锁,其他线程需等待
|
||||
data = self.client.db_read(db_number, offset, size)
|
||||
@ -155,16 +155,10 @@ class Snap7Client:
|
||||
return False
|
||||
|
||||
try:
|
||||
with self.lock:
|
||||
values = int(data)
|
||||
value = bytearray(0)
|
||||
if isinstance(values, int):
|
||||
set_int(value, offset, values)
|
||||
data = value
|
||||
|
||||
self.client.db_write(db_number, offset, data)
|
||||
self.logger.debug(f"Wrote {len(data)} bytes to DB{db_number} offset {offset}")
|
||||
return True
|
||||
self.client.db_write(db_number, offset, data)
|
||||
self.logger.debug(f"Wrote {len(data)} bytes to DB{db_number} offset {offset}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Write DB{db_number} error: {e}")
|
||||
self.connected = False
|
||||
@ -214,13 +208,6 @@ class Snap7Client:
|
||||
|
||||
try:
|
||||
with self.lock:
|
||||
data = data.decode('utf-8')
|
||||
# 将字符串安全转换为字典
|
||||
data_dict = ast.literal_eval(data) # 输出: {0: True}
|
||||
value = bytearray(offset + 1)
|
||||
for bit, val in data_dict.items():
|
||||
set_bool(value, offset, bit, val)
|
||||
data = value
|
||||
|
||||
self.client.db_write(db_number, offset, data)
|
||||
self.logger.debug(f"Wrote {len(data)} bytes to DB{db_number} offset {offset}")
|
||||
@ -255,4 +242,207 @@ class Snap7Client:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Write DB{db_number} error: {e}")
|
||||
self.connected = False
|
||||
return False
|
||||
|
||||
def read_generic(self, db_number, offset, data_type, count=1):
|
||||
"""
|
||||
通用读取接口,支持多种数据类型
|
||||
Args:
|
||||
db_number: DB块编号
|
||||
offset: 起始偏移量(字节或位,对于bool类型)
|
||||
data_type: 数据类型 ('bool', 'byte', 'int', 'word', 'real', 'dint', 'dword')
|
||||
count: 要读取的数据个数
|
||||
Returns:
|
||||
解析后的数据(单个值或值列表),失败返回None
|
||||
"""
|
||||
if not self.connected and not self.connect():
|
||||
self.logger.warning(f"Read failed: not connected to {self.ip}")
|
||||
return None
|
||||
|
||||
try:
|
||||
if data_type == 'bool':
|
||||
# 对于bool,offset是位偏移
|
||||
byte_offset = offset // 8
|
||||
bit_offset = offset % 8
|
||||
# 计算需要读取的字节数
|
||||
last_bit = bit_offset + count - 1
|
||||
last_byte = last_bit // 8
|
||||
total_bytes = last_byte - byte_offset + 1
|
||||
|
||||
# 读取原始字节数据
|
||||
data = self.read_db(db_number, byte_offset, total_bytes)
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
# 解析bool值
|
||||
result = []
|
||||
for i in range(count):
|
||||
current_bit = bit_offset + i
|
||||
byte_idx = current_bit // 8
|
||||
bit_idx = current_bit % 8
|
||||
result.append(bool(data[byte_idx] & (1 << bit_idx)))
|
||||
|
||||
return result[0] if count == 1 else result
|
||||
|
||||
elif data_type == 'byte':
|
||||
data = self.read_db(db_number, offset, count)
|
||||
if data is None:
|
||||
return None
|
||||
return [data[i] for i in range(count)] if count > 1 else data[0]
|
||||
|
||||
elif data_type in ['int', 'word']:
|
||||
total_bytes = 2 * count
|
||||
data = self.read_db(db_number, offset, total_bytes)
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
result = []
|
||||
for i in range(count):
|
||||
if data_type == 'int':
|
||||
result.append(get_int(data, i * 2))
|
||||
else: # word
|
||||
result.append(get_word(data, i * 2))
|
||||
return result[0] if count == 1 else result
|
||||
|
||||
elif data_type in ['dint', 'dword', 'real']:
|
||||
total_bytes = 4 * count
|
||||
data = self.read_db(db_number, offset, total_bytes)
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
result = []
|
||||
for i in range(count):
|
||||
if data_type == 'dint':
|
||||
result.append(get_dint(data, i * 4))
|
||||
elif data_type == 'dword':
|
||||
result.append(get_dword(data, i * 4))
|
||||
else: # real
|
||||
result.append(get_real(data, i * 4))
|
||||
return result[0] if count == 1 else result
|
||||
|
||||
else:
|
||||
self.logger.error(f"Unsupported data type: {data_type}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reading {data_type} from DB{db_number} offset {offset}: {e}")
|
||||
return None
|
||||
|
||||
def write_generic(self, db_number, offset, data_type, value):
|
||||
"""
|
||||
通用写入接口,支持多种数据类型
|
||||
Args:
|
||||
db_number: DB块编号
|
||||
offset: 起始偏移量(字节或位,对于bool类型)
|
||||
data_type: 数据类型 ('bool', 'byte', 'int', 'word', 'real', 'dint', 'dword')
|
||||
value: 要写入的值(可以是单个值或列表)
|
||||
Returns:
|
||||
bool: 是否写入成功
|
||||
"""
|
||||
if not self.connected and not self.connect():
|
||||
self.logger.warning(f"Write failed: not connected to {self.ip}")
|
||||
return False
|
||||
|
||||
try:
|
||||
if data_type == 'bool':
|
||||
# 对于bool,offset是位偏移
|
||||
byte_offset = offset // 8
|
||||
bit_offset = offset % 8
|
||||
|
||||
# 读取当前字节
|
||||
current_byte = self.read_db(db_number, byte_offset, 1)
|
||||
if current_byte is None:
|
||||
return False
|
||||
|
||||
# 修改特定位
|
||||
if isinstance(value, list):
|
||||
# 多个bool值
|
||||
for i, val in enumerate(value):
|
||||
current_bit = bit_offset + i
|
||||
byte_idx = current_bit // 8
|
||||
bit_idx = current_bit % 8
|
||||
|
||||
if val:
|
||||
current_byte[0] |= (1 << bit_idx)
|
||||
else:
|
||||
current_byte[0] &= ~(1 << bit_idx)
|
||||
else:
|
||||
# 单个bool值
|
||||
if value:
|
||||
current_byte[0] |= (1 << bit_offset)
|
||||
else:
|
||||
current_byte[0] &= ~(1 << bit_offset)
|
||||
|
||||
# 写回修改后的字节
|
||||
return self.write_db_bool(db_number, byte_offset, current_byte)
|
||||
|
||||
elif data_type == 'byte':
|
||||
if isinstance(value, list):
|
||||
# 批量写入
|
||||
for i, val in enumerate(value):
|
||||
if val < 0 or val > 255:
|
||||
self.logger.error(f"Byte value out of range: {val}")
|
||||
return False
|
||||
if not self.write_db(db_number, offset + i, bytes([val])):
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
# 单个字节
|
||||
if value < 0 or value > 255:
|
||||
self.logger.error(f"Byte value out of range: {value}")
|
||||
return False
|
||||
return self.write_db(db_number, offset, bytes([value]))
|
||||
|
||||
elif data_type in ['int', 'word']:
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
for i, val in enumerate(value):
|
||||
# 确保int值在范围内
|
||||
if data_type == 'int' and (val < -32768 or val > 32767):
|
||||
self.logger.error(f"Int value out of range: {val}")
|
||||
return False
|
||||
elif data_type == 'word' and (val < 0 or val > 65535):
|
||||
self.logger.error(f"Word value out of range: {val}")
|
||||
return False
|
||||
|
||||
data = bytearray(2)
|
||||
if data_type == 'int':
|
||||
set_int(data, 0, val)
|
||||
else:
|
||||
set_word(data, 0, val)
|
||||
|
||||
if not self.write_db(db_number, offset + i * 2, data):
|
||||
return False
|
||||
return True
|
||||
|
||||
elif data_type in ['dint', 'dword', 'real']:
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
for i, val in enumerate(value):
|
||||
data = bytearray(4)
|
||||
if data_type == 'dint':
|
||||
if val < -2147483648 or val > 2147483647:
|
||||
self.logger.error(f"DInt value out of range: {val}")
|
||||
return False
|
||||
set_dint(data, 0, val)
|
||||
elif data_type == 'dword':
|
||||
if val < 0 or val > 4294967295:
|
||||
self.logger.error(f"DWord value out of range: {val}")
|
||||
return False
|
||||
set_dword(data, 0, val)
|
||||
else: # real
|
||||
set_real(data, 0, float(val))
|
||||
|
||||
if not self.write_db(db_number, offset + i * 4, data):
|
||||
return False
|
||||
return True
|
||||
|
||||
else:
|
||||
self.logger.error(f"Unsupported data type: {data_type}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error writing {data_type} to DB{db_number} offset {offset}: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user