@@ -208,11 +221,20 @@ class APIServer:
@self.app.route("/api/status", endpoint="system_status")
def system_status():
"""获取系统状态信息"""
+ plc_statuses = {}
+ 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]))
+ if self.cache_manager.plc_last_connected[plc_name] > 0 else "Never"
+ }
+
return jsonify({
"status": "running",
"start_time": self.start_time,
"plc_count": len(self.config_manager.get_config().get("plcs", [])),
- "cache_size": sum(len(area["data"]) for plc in self.cache_manager.cache.values() for area in plc.values())
+ "cache_size": sum(len(area["data"]) for plc in self.cache_manager.cache.values() for area in plc.values()),
+ "plc_statuses": plc_statuses
})
# ===========================
@@ -279,10 +301,21 @@ class APIServer:
.config-help h3 {
margin-top: 0;
}
+ .doc-link {
+ margin-top: 10px;
+ padding: 10px;
+ background-color: #e9f7fe;
+ border-radius: 4px;
+ }
Edit the configuration JSON below. Be careful with the syntax.
@@ -488,6 +521,592 @@ class APIServer:
"message": f"Error saving config: {str(e)}"
}), 500
+ # ===========================
+ # 新增 API 文档接口
+ # ===========================
+ @self.app.route("/api/doc", endpoint="api_doc")
+ def api_doc():
+ """API文档页面"""
+ html = """
+
+
+
PLC Gateway API Documentation
+
+
+
+
PLC Gateway API Documentation
+
+
+
+
Status API
+
+
+
System Status
+
+ GET
+ /api/status
+
+
获取系统状态信息,包括启动时间、PLC数量和缓存大小。
+
+
响应示例
+
+ {
+ "status": "running",
+ "start_time": "2023-10-30 14:30:22",
+ "plc_count": 2,
+ "cache_size": 11000,
+ "plc_statuses": {
+ "PLC1": {
+ "status": "connected",
+ "last_connected": "2023-10-30 14:35:10"
+ },
+ "PLC2": {
+ "status": "disconnected",
+ "last_connected": "Never"
+ }
+ }
+ }
+
+
+
+
+
Area Status
+
+
获取指定PLC区域的状态信息。
+
+
路径参数
+
+
+ | 参数 |
+ 描述 |
+
+
+ | plc_name |
+ PLC名称(如PLC1) |
+
+
+ | area_name |
+ 区域名称(如DB100_Read) |
+
+
+
+
响应示例
+
+ {
+ "status": "connected",
+ "plc_connection_status": "connected",
+ "last_update": 1698754321.456,
+ "last_update_formatted": "2023-10-30 14:12:01",
+ "size": 4000,
+ "type": "read"
+ }
+
+
+
+
Data API
+
+
+
Single Read
+
+
从指定区域读取数据。
+
+
路径参数
+
+
+ | 参数 |
+ 描述 |
+
+
+ | plc_name |
+ PLC名称(如PLC1) |
+
+
+ | area_name |
+ 区域名称(如DB100_Read) |
+
+
+ | offset |
+ 起始偏移量(字节) |
+
+
+ | length |
+ 读取长度(字节) |
+
+
+
+
响应示例
+
+ {
+ "status": "success",
+ "plc_name": "PLC1",
+ "area_name": "DB100_Read",
+ "offset": 0,
+ "length": 4,
+ "data": [0, 0, 123, 45],
+ "plc_connection_status": "connected",
+ "last_update": 1698754321.456,
+ "last_update_formatted": "2023-10-30 14:12:01"
+ }
+
+
+
+
+
Single Write
+
+
向指定区域写入数据。
+
+
路径参数
+
+
+ | 参数 |
+ 描述 |
+
+
+ | plc_name |
+ PLC名称(如PLC1) |
+
+
+ | area_name |
+ 区域名称(如DB100_Write) |
+
+
+ | offset |
+ 起始偏移量(字节) |
+
+
+
+
请求体
+
原始二进制数据
+
+
响应示例
+
+ {
+ "status": "success",
+ "plc_name": "PLC1",
+ "area_name": "DB100_Write",
+ "offset": 0,
+ "length": 4,
+ "plc_connection_status": "connected",
+ "last_update": 1698754350.789,
+ "last_update_formatted": "2023-10-30 14:12:30"
+ }
+
+
+
+
+
Batch Read
+
+ POST
+ /api/batch_read
+
+
批量读取多个区域的数据。
+
+
请求体
+
+
+ | 字段 |
+ 类型 |
+ 必需 |
+ 描述 |
+
+
+ | plc_name |
+ string |
+ 是 |
+ PLC名称(与配置中一致) |
+
+
+ | area_name |
+ string |
+ 是 |
+ 区域名称(与配置中一致) |
+
+
+ | offset |
+ number |
+ 否 |
+ 起始偏移量(字节),默认为0 |
+
+
+ | length |
+ number |
+ 否 |
+ 读取长度(字节),不提供则读取整个区域 |
+
+
+
+
请求示例
+
+ [
+ {
+ "plc_name": "PLC1",
+ "area_name": "DB100_Read",
+ "offset": 0,
+ "length": 4
+ },
+ {
+ "plc_name": "PLC1",
+ "area_name": "DB202_Params",
+ "offset": 10,
+ "length": 2
+ }
+ ]
+
+
+
响应示例
+
+ [
+ {
+ "plc_name": "PLC1",
+ "area_name": "DB100_Read",
+ "status": "success",
+ "plc_connection_status": "connected",
+ "last_update": 1698754321.456,
+ "last_update_formatted": "2023-10-30 14:12:01",
+ "offset": 0,
+ "length": 4,
+ "data": [0, 0, 123, 45]
+ },
+ {
+ "plc_name": "PLC1",
+ "area_name": "DB202_Params",
+ "status": "success",
+ "plc_connection_status": "connected",
+ "last_update": 1698754322.123,
+ "last_update_formatted": "2023-10-30 14:12:02",
+ "offset": 10,
+ "length": 2,
+ "data": [255, 0]
+ }
+ ]
+
+
+
+
+
Batch Write
+
+ POST
+ /api/batch_write
+
+
批量写入多个区域的数据。
+
+
请求体
+
+
+ | 字段 |
+ 类型 |
+ 必需 |
+ 描述 |
+
+
+ | plc_name |
+ string |
+ 是 |
+ PLC名称(与配置中一致) |
+
+
+ | area_name |
+ string |
+ 是 |
+ 区域名称(与配置中一致) |
+
+
+ | offset |
+ number |
+ 是 |
+ 起始偏移量(字节) |
+
+
+ | data |
+ array |
+ 是 |
+ 要写入的数据(字节数组) |
+
+
+
+
请求示例
+
+ [
+ {
+ "plc_name": "PLC1",
+ "area_name": "DB100_Write",
+ "offset": 0,
+ "data": [1, 2, 3, 4]
+ },
+ {
+ "plc_name": "PLC1",
+ "area_name": "DB202_Params",
+ "offset": 10,
+ "data": [255, 0]
+ }
+ ]
+
+
+
响应示例
+
+ [
+ {
+ "plc_name": "PLC1",
+ "area_name": "DB100_Write",
+ "status": "success",
+ "plc_connection_status": "connected",
+ "last_update": 1698754350.789,
+ "last_update_formatted": "2023-10-30 14:12:30",
+ "offset": 0,
+ "length": 4
+ },
+ {
+ "plc_name": "PLC1",
+ "area_name": "DB202_Params",
+ "status": "success",
+ "plc_connection_status": "connected",
+ "last_update": 1698754351.234,
+ "last_update_formatted": "2023-10-30 14:12:31",
+ "offset": 10,
+ "length": 2
+ }
+ ]
+
+
+
+
+
Parsed Data
+
+
获取解析后的数据(如果配置了结构)。
+
+
路径参数
+
+
+ | 参数 |
+ 描述 |
+
+
+ | plc_name |
+ PLC名称(如PLC1) |
+
+
+ | area_name |
+ 区域名称(如DB100_Read) |
+
+
+
+
响应示例(配置了解析结构)
+
+ {
+ "parsed": {
+ "temperature": 25.5,
+ "pressure": 100,
+ "status": true
+ },
+ "raw_data": [0, 0, 128, 65, 0, 100],
+ "status": "connected",
+ "plc_connection_status": "connected",
+ "last_update": 1698754321.456,
+ "last_update_formatted": "2023-10-30 14:12:01"
+ }
+
+
+
响应示例(未配置解析结构)
+
+ {
+ "raw_data": [0, 0, 128, 65, 0, 100],
+ "status": "connected",
+ "plc_connection_status": "connected",
+ "last_update": 1698754321.456,
+ "last_update_formatted": "2023-10-30 14:12:01"
+ }
+
+
+
+
Configuration API
+
+
+
Get Configuration
+
+ GET
+ /api/config
+
+
获取当前配置。
+
+
认证要求
+
需要Basic Auth认证
+
+
响应示例
+
+ {
+ "plcs": [
+ {
+ "name": "PLC1",
+ "ip": "192.168.0.10",
+ "rack": 0,
+ "slot": 1,
+ "areas": [
+ {
+ "name": "DB100_Read",
+ "type": "read",
+ "db_number": 100,
+ "offset": 0,
+ "size": 4000,
+ "structure": [
+ {"name": "temperature", "type": "real", "offset": 0},
+ {"name": "pressure", "type": "int", "offset": 4}
+ ]
+ }
+ ]
+ }
+ ]
+ }
+
+
+
+
+
Validate Configuration
+
+ POST
+ /api/config/validate
+
+
验证配置是否有效。
+
+
认证要求
+
需要Basic Auth认证
+
+
请求体
+
要验证的配置JSON
+
+
响应示例(有效)
+
+ {
+ "valid": true
+ }
+
+
+
响应示例(无效)
+
+ {
+ "valid": false,
+ "message": "Invalid configuration: 'ip' is a required property"
+ }
+
+
+
+
+
Save Configuration
+
+ POST
+ /api/config
+
+
保存配置。
+
+
查询参数
+
+
+ | 参数 |
+ 描述 |
+
+
+ | reload |
+ 是否立即重载配置(true/false) |
+
+
+
+
认证要求
+
需要Basic Auth认证
+
+
请求体
+
要保存的配置JSON
+
+
响应示例
+
+ {
+ "success": true,
+ "message": "Configuration saved and reload requested"
+ }
+
+
+
+
+
+
+ """
+ return render_template_string(html)
+
# ===========================
# 数据访问API
# ===========================
@@ -495,16 +1114,27 @@ class APIServer:
@self.app.route("/api/read/
///", methods=["GET"], endpoint="single_read")
def single_read(plc_name, area_name, offset, length):
"""从指定区域读取数据"""
- data, error = self.cache_manager.read_area(plc_name, area_name, offset, length)
+ data, error, plc_status, update_time = self.cache_manager.read_area(plc_name, area_name, offset, length)
if error:
- return jsonify({"status": "error", "message": error}), 400
+ 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,
"length": length,
- "data": list(data)
+ "data": list(data),
+ "plc_connection_status": plc_status,
+ "last_update": update_time,
+ "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time))
})
# 单个写入接口
@@ -513,17 +1143,35 @@ class APIServer:
"""向指定区域写入数据"""
data = request.data
if not data:
- return jsonify({"status": "error", "message": "No data provided"}), 400
+ # 如果没有提供数据,返回错误
+ return jsonify({
+ "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 = self.cache_manager.write_area(plc_name, area_name, offset, data)
+ success, error, plc_status, update_time = self.cache_manager.write_area(plc_name, area_name, offset, data)
if error:
- return jsonify({"status": "error", "message": error}), 400
+ 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,
- "length": len(data)
+ "length": len(data),
+ "plc_connection_status": plc_status,
+ "last_update": update_time,
+ "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time))
})
# 批量读取接口
@@ -531,32 +1179,85 @@ class APIServer:
def batch_read():
"""批量读取多个区域的数据"""
try:
+ # 确保是JSON请求
+ if not request.is_json:
+ return jsonify({
+ "status": "error",
+ "message": "Request must be JSON (Content-Type: application/json)",
+ "plc_connection_status": "unknown",
+ "last_update": 0,
+ "last_update_formatted": "N/A"
+ }), 400
+
requests = request.json
if not isinstance(requests, list):
- return jsonify({"status": "error", "message": "Request must be a JSON array"}), 400
+ return jsonify({
+ "status": "error",
+ "message": "Request must be a JSON array",
+ "plc_connection_status": "unknown",
+ "last_update": 0,
+ "last_update_formatted": "N/A"
+ }), 400
+
+ # 添加详细日志
+ self.logger.info(f"Received batch read request: {json.dumps(requests)}")
+
results = self.cache_manager.batch_read(requests)
return jsonify(results)
except Exception as e:
- return jsonify({"status": "error", "message": str(e)}), 400
+ self.logger.error(f"Batch read error: {str(e)}", exc_info=True)
+ return jsonify({
+ "status": "error",
+ "message": f"Internal server error: {str(e)}",
+ "plc_connection_status": "unknown",
+ "last_update": 0,
+ "last_update_formatted": "N/A"
+ }), 500
# 批量写入接口
@self.app.route("/api/batch_write", methods=["POST"], endpoint="batch_write")
def batch_write():
"""批量写入多个区域的数据"""
try:
+ if not request.is_json:
+ return jsonify({
+ "status": "error",
+ "message": "Request must be JSON (Content-Type: application/json)",
+ "plc_connection_status": "unknown",
+ "last_update": 0,
+ "last_update_formatted": "N/A"
+ }), 400
+
requests = request.json
if not isinstance(requests, list):
- return jsonify({"status": "error", "message": "Request must be a JSON array"}), 400
+ return jsonify({
+ "status": "error",
+ "message": "Request must be a JSON array",
+ "plc_connection_status": "unknown",
+ "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:
- return jsonify({"status": "error", "message": str(e)}), 400
+ self.logger.error(f"Batch write error: {str(e)}", exc_info=True)
+ return jsonify({
+ "status": "error",
+ "message": f"Internal server error: {str(e)}",
+ "plc_connection_status": "unknown",
+ "last_update": 0,
+ "last_update_formatted": "N/A"
+ }), 500
# 区域状态检查
@self.app.route("/api/status//", methods=["GET"], endpoint="area_status")
def area_status(plc_name, area_name):
"""获取区域状态"""
- return jsonify(self.cache_manager.get_area_status(plc_name, area_name))
+ status = self.cache_manager.get_area_status(plc_name, area_name)
+ return jsonify(status)
# 获取解析后的数据
@self.app.route("/api/data//", methods=["GET"], endpoint="get_parsed_data")
diff --git a/gateway/cache_manager.py b/gateway/cache_manager.py
index 9815846..2ff30c6 100644
--- a/gateway/cache_manager.py
+++ b/gateway/cache_manager.py
@@ -22,8 +22,9 @@ class CacheManager:
self.running = False
self.lock = threading.Lock()
self.thread = None
- self.last_update = {}
- self.plc_last_connected = {} # 跟踪PLC最后连接时间
+ self.last_update = {} # 区域级最后更新时间
+ self.plc_last_connected = {} # PLC级最后连接时间
+ self.plc_connection_status = {} # PLC连接状态
self.logger = logging.getLogger("CacheManager")
self.init_cache()
@@ -34,6 +35,7 @@ class CacheManager:
self.cache[plc_name] = {}
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"]
@@ -60,34 +62,39 @@ class CacheManager:
# 检查PLC连接状态
plc_connected = client.connected
+ # 更新PLC连接状态
+ with self.lock:
+ if plc_connected:
+ self.plc_last_connected[plc_name] = time.time()
+ self.plc_connection_status[plc_name] = "connected"
+ else:
+ if self.plc_last_connected[plc_name] == 0:
+ 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"])
- # 验证数据有效性
- if data and len(data) == area["size"]:
- with self.lock:
+
+ # 更新区域状态基于PLC连接状态和读取结果
+ with self.lock:
+ if plc_connected and data and len(data) == area["size"]:
self.cache[plc_name][name]["data"] = bytearray(data)
self.cache[plc_name][name]["status"] = "connected"
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")
+ else:
+ self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name]
+ # 如果之前有数据,保留旧数据但标记状态
+ if self.last_update[plc_name][name] > 0:
+ self.logger.info(f"PLC {plc_name} area {name} disconnected but keeping last valid data")
except Exception as e:
with self.lock:
- self.cache[plc_name][name]["status"] = "disconnected"
- 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"
+ self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name]
+ self.logger.warning(f"Error updating status for {plc_name}/{name}: {e}")
time.sleep(self.refresh_interval)
except Exception as e:
@@ -122,6 +129,16 @@ class CacheManager:
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 = {}
@@ -130,19 +147,18 @@ class CacheManager:
summary[plc_name] = {}
for area_name, area in areas.items():
last_update = self.last_update[plc_name][area_name]
- plc_last_connected = self.plc_last_connected[plc_name]
+ plc_status = self.plc_connection_status.get(plc_name, "unknown")
- # 如果PLC从未连接过,显示特殊状态
- if plc_last_connected == 0:
- status = "never_connected"
- # 如果PLC断开连接超过5秒,标记为断开
- elif time.time() - plc_last_connected > 5:
- status = "disconnected"
- else:
- status = area["status"]
+ # 区域状态应与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": status,
+ "status": area_status,
+ "plc_connection_status": plc_status,
"last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never",
"size": area["size"],
"type": area["type"]
@@ -156,18 +172,21 @@ class CacheManager:
if not area:
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"]
-
+ 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": status,
- "last_update": self.last_update[plc_name][area_name],
+ "status": area_status,
+ "plc_connection_status": plc_status,
+ "last_update": last_update,
+ "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never",
"size": area["size"],
"type": area["type"]
}
@@ -177,12 +196,18 @@ class CacheManager:
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
- return None, "Area not found"
+ return None, "Area not found", "unknown", 0
if offset + length > area["size"]:
- return None, "Offset out of bounds"
+ 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)
# 验证数据有效性
@@ -190,118 +215,178 @@ class CacheManager:
# 更新缓存中的这部分数据
for i in range(length):
area["data"][offset + i] = data[i]
- self.last_update[plc_name][area_name] = time.time()
- self.plc_last_connected[plc_name] = time.time()
+ update_time = time.time()
+ self.last_update[plc_name][area_name] = update_time
area["status"] = "connected"
- return data, None
+
+ return data, None, plc_status, update_time
else:
- area["status"] = "disconnected"
- return None, "Invalid data returned"
+ area["status"] = plc_status
+ return None, "Invalid data returned", plc_status, 0
except Exception as e:
- area["status"] = "disconnected"
+ area["status"] = plc_status
self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}")
- return None, f"Read failed: {str(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"
+ return False, "Area not found", "unknown", 0
if area["type"] not in ["write", "read_write"]:
- return False, "Area is read-only"
+ 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"]:
- return False, "Offset out of bounds"
+ 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:
# 更新缓存中的这部分数据
for i in range(len(data)):
area["data"][offset + i] = data[i]
- self.last_update[plc_name][area_name] = time.time()
- self.plc_last_connected[plc_name] = time.time()
+ update_time = time.time()
+ self.last_update[plc_name][area_name] = update_time
area["status"] = "connected (last write)"
- return True, None
+
+ return True, None, plc_status, update_time
else:
- area["status"] = "disconnected"
- return False, "Write failed"
+ area["status"] = plc_status
+ return False, "Write failed", plc_status, 0
except Exception as e:
- area["status"] = "disconnected"
+ area["status"] = plc_status
self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}")
- return False, f"Write failed: {str(e)}"
+ return False, f"Write failed: {str(e)}", plc_status, 0
def batch_read(self, requests):
"""批量读取"""
results = []
- for req in requests:
- plc_name = req["plc_name"]
- area_name = req["area_name"]
- offset = req.get("offset", 0)
- length = req.get("length", None)
-
- area = self.cache.get(plc_name, {}).get(area_name)
- if not area:
- results.append({
- "plc_name": plc_name,
- "area_name": area_name,
- "status": "error",
- "message": "Area not found"
- })
- continue
+ with self.lock:
+ for req in requests:
+ plc_name = req["plc_name"]
+ area_name = req["area_name"]
+ offset = req.get("offset", 0)
+ length = req.get("length", None)
- # 如果未指定length,读取整个区域
- if length is None:
- length = area["size"] - offset
-
- data, error = self.read_area(plc_name, area_name, offset, length)
- if error:
- results.append({
- "plc_name": plc_name,
- "area_name": area_name,
- "status": "error",
- "message": error
- })
- else:
- results.append({
- "plc_name": plc_name,
- "area_name": area_name,
- "status": "success",
- "offset": offset,
- "length": length,
- "data": list(data)
- })
+ # 获取PLC连接状态
+ plc_status = self.plc_connection_status.get(plc_name, "unknown")
+
+ # 如果PLC未连接,直接返回错误
+ if plc_status != "connected":
+ results.append({
+ "plc_name": plc_name,
+ "area_name": area_name,
+ "status": "error",
+ "plc_connection_status": plc_status,
+ "last_update": 0,
+ "last_update_formatted": "N/A",
+ "message": f"PLC not connected (status: {plc_status})"
+ })
+ continue
+
+ area = self.cache.get(plc_name, {}).get(area_name)
+ if not area:
+ results.append({
+ "plc_name": plc_name,
+ "area_name": area_name,
+ "status": "error",
+ "plc_connection_status": plc_status,
+ "last_update": 0,
+ "last_update_formatted": "N/A",
+ "message": "Area not found"
+ })
+ continue
+
+ # 如果未指定length,读取整个区域
+ if length is None:
+ length = area["size"] - offset
+
+ data, error, _, update_time = self.read_area(plc_name, area_name, offset, length)
+ if error:
+ results.append({
+ "plc_name": plc_name,
+ "area_name": area_name,
+ "status": "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",
+ "message": error
+ })
+ else:
+ results.append({
+ "plc_name": plc_name,
+ "area_name": area_name,
+ "status": "success",
+ "plc_connection_status": plc_status,
+ "last_update": update_time,
+ "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)),
+ "offset": offset,
+ "length": length,
+ "data": list(data)
+ })
return results
def batch_write(self, requests):
"""批量写入"""
results = []
- for req in requests:
- plc_name = req["plc_name"]
- area_name = req["area_name"]
- offset = req["offset"]
- data = bytes(req["data"])
-
- success, error = self.write_area(plc_name, area_name, offset, data)
- if error:
- results.append({
- "plc_name": plc_name,
- "area_name": area_name,
- "status": "error",
- "message": error,
- "offset": offset
- })
- else:
- results.append({
- "plc_name": plc_name,
- "area_name": area_name,
- "status": "success",
- "offset": offset,
- "length": len(data)
- })
+ with self.lock:
+ for req in requests:
+ plc_name = req["plc_name"]
+ area_name = req["area_name"]
+ offset = req["offset"]
+ data = bytes(req["data"])
+
+ # 获取PLC连接状态
+ plc_status = self.plc_connection_status.get(plc_name, "unknown")
+
+ # 如果PLC未连接,直接返回错误
+ if plc_status != "connected":
+ results.append({
+ "plc_name": plc_name,
+ "area_name": area_name,
+ "status": "error",
+ "plc_connection_status": plc_status,
+ "last_update": 0,
+ "last_update_formatted": "N/A",
+ "message": f"PLC not connected (status: {plc_status})",
+ "offset": offset
+ })
+ continue
+
+ success, error, _, update_time = self.write_area(plc_name, area_name, offset, data)
+ if error:
+ results.append({
+ "plc_name": plc_name,
+ "area_name": area_name,
+ "status": "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",
+ "message": error,
+ "offset": offset
+ })
+ else:
+ results.append({
+ "plc_name": plc_name,
+ "area_name": area_name,
+ "status": "success",
+ "plc_connection_status": plc_status,
+ "last_update": update_time,
+ "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)),
+ "offset": offset,
+ "length": len(data)
+ })
return results
def get_parsed_data(self, plc_name, area_name):
@@ -313,22 +398,29 @@ class CacheManager:
if not area:
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"]
-
+ 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:
- return parse_data(area["data"], structure)
+ parsed = parse_data(area["data"], structure)
+ parsed["status"] = area_status
+ parsed["plc_connection_status"] = plc_status
+ parsed["last_update"] = last_update
+ parsed["last_update_formatted"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never"
+ return parsed
else:
return {
"raw_data": list(area["data"]),
- "status": status,
- "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"
+ "status": area_status,
+ "plc_connection_status": plc_status,
+ "last_update": last_update,
+ "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never"
}
\ No newline at end of file
diff --git a/gateway/data_parser.py b/gateway/data_parser.py
index 38890b3..0c4e236 100644
--- a/gateway/data_parser.py
+++ b/gateway/data_parser.py
@@ -1,4 +1,5 @@
from struct import unpack
+import time
def parse_data(data, structure):
"""解析结构化数据"""
diff --git a/gateway/snap7_client.py b/gateway/snap7_client.py
index 3bc94dc..2c7a7fd 100644
--- a/gateway/snap7_client.py
+++ b/gateway/snap7_client.py
@@ -81,11 +81,11 @@ class Snap7Client:
size: 读取字节数
Returns:
- bytearray: 读取的数据
+ bytearray: 读取的数据,如果失败返回None
"""
if not self.connected and not self.connect():
self.logger.warning(f"Read failed: not connected to {self.ip}")
- return b'\x00' * size
+ return None # 返回None而不是零填充数据
try:
with self.lock:
@@ -94,12 +94,12 @@ class Snap7Client:
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 None
return data
except Exception as e:
self.logger.error(f"Read DB{db_number} error: {e}")
self.connected = False
- return b'\x00' * size
+ return None
def write_db(self, db_number, offset, data):
"""