修复状态显示
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,14 +2,14 @@ from flask import Flask, jsonify, request, render_template_string, Response
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from config_manager import ConfigManager
|
from config_manager import ConfigManager
|
||||||
|
import logging
|
||||||
|
|
||||||
class APIServer:
|
class APIServer:
|
||||||
"""REST API服务器,提供PLC数据访问和配置管理功能"""
|
"""REST API服务器,提供PLC数据访问和配置管理功能"""
|
||||||
|
|
||||||
def __init__(self, cache_manager, config_path="../config/config.json"):
|
def __init__(self, cache_manager, config_path="config/config.json"):
|
||||||
"""
|
"""
|
||||||
初始化API服务器
|
初始化API服务器
|
||||||
|
|
||||||
@ -20,12 +20,14 @@ class APIServer:
|
|||||||
self.cache_manager = cache_manager
|
self.cache_manager = cache_manager
|
||||||
self.config_manager = ConfigManager(config_path)
|
self.config_manager = ConfigManager(config_path)
|
||||||
self.app = Flask(__name__)
|
self.app = Flask(__name__)
|
||||||
self.setup_routes()
|
self.logger = logging.getLogger("APIServer")
|
||||||
self.auth_enabled = True # 可通过配置关闭认证
|
self.auth_enabled = True # 可通过配置关闭认证
|
||||||
self.username = "admin"
|
self.username = "admin"
|
||||||
self.password = "admin123" # 实际应用中应从安全存储获取
|
self.password = "admin123" # 实际应用中应从安全存储获取
|
||||||
self.start_time = time.strftime("%Y-%m-%d %H:%M:%S")
|
self.start_time = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
self.logger = logging.getLogger("APIServer")
|
|
||||||
|
# 在初始化方法中调用 setup_routes
|
||||||
|
self.setup_routes()
|
||||||
|
|
||||||
def check_auth(self, username, password):
|
def check_auth(self, username, password):
|
||||||
"""验证用户名和密码"""
|
"""验证用户名和密码"""
|
||||||
@ -59,6 +61,14 @@ class APIServer:
|
|||||||
summary[plc_name] = {}
|
summary[plc_name] = {}
|
||||||
for area_name, area in areas.items():
|
for area_name, area in areas.items():
|
||||||
last_update = self.cache_manager.last_update[plc_name][area_name]
|
last_update = self.cache_manager.last_update[plc_name][area_name]
|
||||||
|
plc_last_connected = self.cache_manager.plc_last_connected.get(plc_name, 0)
|
||||||
|
|
||||||
|
# 确定状态
|
||||||
|
if plc_last_connected == 0:
|
||||||
|
status = "never_connected"
|
||||||
|
elif time.time() - plc_last_connected > 5:
|
||||||
|
status = "disconnected"
|
||||||
|
else:
|
||||||
status = area["status"]
|
status = area["status"]
|
||||||
|
|
||||||
summary[plc_name][area_name] = {
|
summary[plc_name][area_name] = {
|
||||||
@ -90,7 +100,7 @@ class APIServer:
|
|||||||
th { background-color: #f2f2f2; }
|
th { background-color: #f2f2f2; }
|
||||||
.status-connected { color: green; font-weight: bold; }
|
.status-connected { color: green; font-weight: bold; }
|
||||||
.status-disconnected { color: red; }
|
.status-disconnected { color: red; }
|
||||||
.status-error { color: orange; }
|
.status-never-connected { color: orange; }
|
||||||
.api-section { margin-top: 30px; }
|
.api-section { margin-top: 30px; }
|
||||||
.api-endpoint { background-color: #f9f9f9; padding: 10px; margin: 5px 0; border-radius: 4px; }
|
.api-endpoint { background-color: #f9f9f9; padding: 10px; margin: 5px 0; border-radius: 4px; }
|
||||||
.config-link { margin-top: 20px; padding: 10px; background-color: #e9f7fe; border-radius: 4px; }
|
.config-link { margin-top: 20px; padding: 10px; background-color: #e9f7fe; border-radius: 4px; }
|
||||||
@ -117,19 +127,25 @@ class APIServer:
|
|||||||
|
|
||||||
for area_name, area in areas.items():
|
for area_name, area in areas.items():
|
||||||
status_class = ""
|
status_class = ""
|
||||||
|
status_text = area["status"]
|
||||||
|
|
||||||
if area["status"] == "connected":
|
if area["status"] == "connected":
|
||||||
status_class = "status-connected"
|
status_class = "status-connected"
|
||||||
|
elif area["status"] == "never_connected":
|
||||||
|
status_class = "status-never-connected"
|
||||||
|
status_text = "Never connected"
|
||||||
elif area["status"] == "disconnected":
|
elif area["status"] == "disconnected":
|
||||||
status_class = "status-disconnected"
|
status_class = "status-disconnected"
|
||||||
|
status_text = "Disconnected"
|
||||||
else:
|
else:
|
||||||
status_class = "status-error"
|
status_class = "status-disconnected"
|
||||||
|
|
||||||
html += f"""
|
html += f"""
|
||||||
<tr>
|
<tr>
|
||||||
<td>{area_name}</td>
|
<td>{area_name}</td>
|
||||||
<td>{area['type']}</td>
|
<td>{area['type']}</td>
|
||||||
<td>{area['size']}</td>
|
<td>{area['size']}</td>
|
||||||
<td class="{status_class}">{area['status']}</td>
|
<td class="{status_class}">{status_text}</td>
|
||||||
<td>{area['last_update']}</td>
|
<td>{area['last_update']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -23,6 +23,7 @@ class CacheManager:
|
|||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.thread = None
|
self.thread = None
|
||||||
self.last_update = {}
|
self.last_update = {}
|
||||||
|
self.plc_last_connected = {} # 跟踪PLC最后连接时间
|
||||||
self.logger = logging.getLogger("CacheManager")
|
self.logger = logging.getLogger("CacheManager")
|
||||||
self.init_cache()
|
self.init_cache()
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ class CacheManager:
|
|||||||
plc_name = plc["name"]
|
plc_name = plc["name"]
|
||||||
self.cache[plc_name] = {}
|
self.cache[plc_name] = {}
|
||||||
self.last_update[plc_name] = {}
|
self.last_update[plc_name] = {}
|
||||||
|
self.plc_last_connected[plc_name] = 0 # 初始化为0(未连接)
|
||||||
|
|
||||||
for area in plc["areas"]:
|
for area in plc["areas"]:
|
||||||
name = area["name"]
|
name = area["name"]
|
||||||
@ -55,20 +57,38 @@ class CacheManager:
|
|||||||
plc_name = plc["name"]
|
plc_name = plc["name"]
|
||||||
client = self.plc_manager.get_plc(plc_name)
|
client = self.plc_manager.get_plc(plc_name)
|
||||||
|
|
||||||
|
# 检查PLC连接状态
|
||||||
|
plc_connected = client.connected
|
||||||
|
|
||||||
for area in plc["areas"]:
|
for area in plc["areas"]:
|
||||||
if area["type"] in ["read", "read_write"]:
|
if area["type"] in ["read", "read_write"]:
|
||||||
name = area["name"]
|
name = area["name"]
|
||||||
try:
|
try:
|
||||||
data = client.read_db(area["db_number"], area["offset"], area["size"])
|
data = client.read_db(area["db_number"], area["offset"], area["size"])
|
||||||
|
# 验证数据有效性
|
||||||
|
if data and len(data) == area["size"]:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.cache[plc_name][name]["data"] = bytearray(data)
|
self.cache[plc_name][name]["data"] = bytearray(data)
|
||||||
self.cache[plc_name][name]["status"] = "connected"
|
self.cache[plc_name][name]["status"] = "connected"
|
||||||
self.last_update[plc_name][name] = time.time()
|
self.last_update[plc_name][name] = time.time()
|
||||||
|
# 更新PLC连接时间
|
||||||
|
self.plc_last_connected[plc_name] = time.time()
|
||||||
|
else:
|
||||||
|
with self.lock:
|
||||||
|
self.cache[plc_name][name]["status"] = "disconnected"
|
||||||
|
self.logger.warning(f"PLC {plc_name} area {name} returned invalid data")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.cache[plc_name][name]["status"] = "disconnected"
|
self.cache[plc_name][name]["status"] = "disconnected"
|
||||||
self.logger.warning(f"PLC {plc_name} area {name} disconnected: {e}")
|
self.logger.warning(f"PLC {plc_name} area {name} disconnected: {e}")
|
||||||
|
|
||||||
|
# 更新所有区域的PLC连接状态
|
||||||
|
if not plc_connected:
|
||||||
|
with self.lock:
|
||||||
|
for area in plc["areas"]:
|
||||||
|
name = area["name"]
|
||||||
|
self.cache[plc_name][name]["status"] = "disconnected"
|
||||||
|
|
||||||
time.sleep(self.refresh_interval)
|
time.sleep(self.refresh_interval)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error in refresh_cache: {e}")
|
self.logger.error(f"Error in refresh_cache: {e}")
|
||||||
@ -110,8 +130,19 @@ class CacheManager:
|
|||||||
summary[plc_name] = {}
|
summary[plc_name] = {}
|
||||||
for area_name, area in areas.items():
|
for area_name, area in areas.items():
|
||||||
last_update = self.last_update[plc_name][area_name]
|
last_update = self.last_update[plc_name][area_name]
|
||||||
|
plc_last_connected = self.plc_last_connected[plc_name]
|
||||||
|
|
||||||
|
# 如果PLC从未连接过,显示特殊状态
|
||||||
|
if plc_last_connected == 0:
|
||||||
|
status = "never_connected"
|
||||||
|
# 如果PLC断开连接超过5秒,标记为断开
|
||||||
|
elif time.time() - plc_last_connected > 5:
|
||||||
|
status = "disconnected"
|
||||||
|
else:
|
||||||
|
status = area["status"]
|
||||||
|
|
||||||
summary[plc_name][area_name] = {
|
summary[plc_name][area_name] = {
|
||||||
"status": area["status"],
|
"status": status,
|
||||||
"last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never",
|
"last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never",
|
||||||
"size": area["size"],
|
"size": area["size"],
|
||||||
"type": area["type"]
|
"type": area["type"]
|
||||||
@ -124,8 +155,18 @@ class CacheManager:
|
|||||||
area = self.cache.get(plc_name, {}).get(area_name)
|
area = self.cache.get(plc_name, {}).get(area_name)
|
||||||
if not area:
|
if not area:
|
||||||
return {"status": "not_found", "message": "PLC or area not found"}
|
return {"status": "not_found", "message": "PLC or area not found"}
|
||||||
|
|
||||||
|
# 检查PLC连接状态
|
||||||
|
plc_last_connected = self.plc_last_connected.get(plc_name, 0)
|
||||||
|
if plc_last_connected == 0:
|
||||||
|
status = "never_connected"
|
||||||
|
elif time.time() - plc_last_connected > 5:
|
||||||
|
status = "disconnected"
|
||||||
|
else:
|
||||||
|
status = area["status"]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": area["status"],
|
"status": status,
|
||||||
"last_update": self.last_update[plc_name][area_name],
|
"last_update": self.last_update[plc_name][area_name],
|
||||||
"size": area["size"],
|
"size": area["size"],
|
||||||
"type": area["type"]
|
"type": area["type"]
|
||||||
@ -144,12 +185,18 @@ class CacheManager:
|
|||||||
client = self.plc_manager.get_plc(plc_name)
|
client = self.plc_manager.get_plc(plc_name)
|
||||||
try:
|
try:
|
||||||
data = client.read_db(area["db_number"], area["offset"] + offset, length)
|
data = client.read_db(area["db_number"], area["offset"] + offset, length)
|
||||||
|
# 验证数据有效性
|
||||||
|
if data and len(data) == length:
|
||||||
# 更新缓存中的这部分数据
|
# 更新缓存中的这部分数据
|
||||||
for i in range(length):
|
for i in range(length):
|
||||||
area["data"][offset + i] = data[i]
|
area["data"][offset + i] = data[i]
|
||||||
self.last_update[plc_name][area_name] = time.time()
|
self.last_update[plc_name][area_name] = time.time()
|
||||||
|
self.plc_last_connected[plc_name] = time.time()
|
||||||
area["status"] = "connected"
|
area["status"] = "connected"
|
||||||
return data, None
|
return data, None
|
||||||
|
else:
|
||||||
|
area["status"] = "disconnected"
|
||||||
|
return None, "Invalid data returned"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
area["status"] = "disconnected"
|
area["status"] = "disconnected"
|
||||||
self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}")
|
self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}")
|
||||||
@ -176,6 +223,7 @@ class CacheManager:
|
|||||||
for i in range(len(data)):
|
for i in range(len(data)):
|
||||||
area["data"][offset + i] = data[i]
|
area["data"][offset + i] = data[i]
|
||||||
self.last_update[plc_name][area_name] = time.time()
|
self.last_update[plc_name][area_name] = time.time()
|
||||||
|
self.plc_last_connected[plc_name] = time.time()
|
||||||
area["status"] = "connected (last write)"
|
area["status"] = "connected (last write)"
|
||||||
return True, None
|
return True, None
|
||||||
else:
|
else:
|
||||||
@ -265,13 +313,22 @@ class CacheManager:
|
|||||||
if not area:
|
if not area:
|
||||||
return {"error": "Area not found"}
|
return {"error": "Area not found"}
|
||||||
|
|
||||||
|
# 检查PLC连接状态
|
||||||
|
plc_last_connected = self.plc_last_connected.get(plc_name, 0)
|
||||||
|
if plc_last_connected == 0:
|
||||||
|
status = "never_connected"
|
||||||
|
elif time.time() - plc_last_connected > 5:
|
||||||
|
status = "disconnected"
|
||||||
|
else:
|
||||||
|
status = area["status"]
|
||||||
|
|
||||||
structure = area.get("structure", [])
|
structure = area.get("structure", [])
|
||||||
if structure:
|
if structure:
|
||||||
return parse_data(area["data"], structure)
|
return parse_data(area["data"], structure)
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"raw_data": list(area["data"]),
|
"raw_data": list(area["data"]),
|
||||||
"status": area["status"],
|
"status": status,
|
||||||
"last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.last_update[plc_name][area_name]))
|
"last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.last_update[plc_name][area_name]))
|
||||||
if self.last_update[plc_name][area_name] > 0 else "Never"
|
if self.last_update[plc_name][area_name] > 0 else "Never"
|
||||||
}
|
}
|
||||||
@ -29,8 +29,18 @@ class Snap7Client:
|
|||||||
self.retry_count = 0
|
self.retry_count = 0
|
||||||
self.logger = logging.getLogger(f"Snap7Client[{ip}]")
|
self.logger = logging.getLogger(f"Snap7Client[{ip}]")
|
||||||
|
|
||||||
|
def is_valid_connection(self):
|
||||||
|
"""检查连接是否真正有效"""
|
||||||
|
try:
|
||||||
|
# 尝试读取PLC的运行状态
|
||||||
|
cpu_state = self.client.get_cpu_state()
|
||||||
|
return cpu_state in [snap7.snap7types.cpu_statuses['S7CpuStatusRun'],
|
||||||
|
snap7.snap7types.cpu_statuses['S7CpuStatusStop']]
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""建立与PLC的连接"""
|
"""建立与PLC的连接并验证"""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
# 指数退避重试
|
# 指数退避重试
|
||||||
if self.retry_count > 0:
|
if self.retry_count > 0:
|
||||||
@ -41,14 +51,19 @@ class Snap7Client:
|
|||||||
self.last_connect_attempt = current_time
|
self.last_connect_attempt = current_time
|
||||||
try:
|
try:
|
||||||
self.client.connect(self.ip, self.rack, self.slot)
|
self.client.connect(self.ip, self.rack, self.slot)
|
||||||
if self.client.get_connected():
|
# 验证连接是否真正有效
|
||||||
|
if self.client.get_connected() and self.is_valid_connection():
|
||||||
self.connected = True
|
self.connected = True
|
||||||
self.retry_count = 0 # 重置重试计数
|
self.retry_count = 0 # 重置重试计数
|
||||||
self.logger.info(f"Connected to PLC {self.ip}")
|
self.logger.info(f"Successfully connected to PLC {self.ip}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self.logger.warning(f"Connection to {self.ip} established but not verified")
|
self.logger.warning(f"Connection to {self.ip} established but PLC is not responding")
|
||||||
|
try:
|
||||||
|
self.client.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.retry_count = min(self.retry_count + 1, self.max_retries)
|
self.retry_count = min(self.retry_count + 1, self.max_retries)
|
||||||
@ -75,6 +90,11 @@ class Snap7Client:
|
|||||||
try:
|
try:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
data = self.client.db_read(db_number, offset, size)
|
data = self.client.db_read(db_number, offset, size)
|
||||||
|
# 验证返回数据的有效性
|
||||||
|
if data is None or len(data) != size:
|
||||||
|
self.connected = False
|
||||||
|
self.logger.error(f"Read DB{db_number} returned invalid data size (expected {size}, got {len(data) if data else 0})")
|
||||||
|
return b'\x00' * size
|
||||||
return data
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Read DB{db_number} error: {e}")
|
self.logger.error(f"Read DB{db_number} error: {e}")
|
||||||
@ -88,7 +108,7 @@ class Snap7Client:
|
|||||||
Args:
|
Args:
|
||||||
db_number: DB编号
|
db_number: DB编号
|
||||||
offset: 起始偏移量
|
offset: 起始偏移量
|
||||||
data: 要写入的数据
|
要写入的数据
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 是否写入成功
|
bool: 是否写入成功
|
||||||
|
|||||||
Reference in New Issue
Block a user