修复状态显示

This commit is contained in:
2025-07-26 02:55:07 +08:00
parent 45deb5131d
commit 2a6b0bce3d
6 changed files with 119 additions and 26 deletions

View File

@ -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>
""" """

View File

@ -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"
} }

View File

@ -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: 是否写入成功