diff --git a/gateway/__pycache__/api_server.cpython-313.pyc b/gateway/__pycache__/api_server.cpython-313.pyc index ca99335..260078d 100644 Binary files a/gateway/__pycache__/api_server.cpython-313.pyc and b/gateway/__pycache__/api_server.cpython-313.pyc differ diff --git a/gateway/__pycache__/cache_manager.cpython-313.pyc b/gateway/__pycache__/cache_manager.cpython-313.pyc index f4dff97..0539d0b 100644 Binary files a/gateway/__pycache__/cache_manager.cpython-313.pyc and b/gateway/__pycache__/cache_manager.cpython-313.pyc differ diff --git a/gateway/__pycache__/snap7_client.cpython-313.pyc b/gateway/__pycache__/snap7_client.cpython-313.pyc index 78258b4..fb99def 100644 Binary files a/gateway/__pycache__/snap7_client.cpython-313.pyc and b/gateway/__pycache__/snap7_client.cpython-313.pyc differ diff --git a/gateway/api_server.py b/gateway/api_server.py index 1ffd84e..cf1d149 100644 --- a/gateway/api_server.py +++ b/gateway/api_server.py @@ -61,18 +61,11 @@ class APIServer: summary[plc_name] = {} for area_name, area in areas.items(): 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"] + plc_status = self.cache_manager.plc_connection_status.get(plc_name, "unknown") 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"] @@ -101,10 +94,14 @@ class APIServer: .status-connected { color: green; font-weight: bold; } .status-disconnected { color: red; } .status-never-connected { color: orange; } + .plc-connected { background-color: #d4edda; } + .plc-disconnected { background-color: #f8d7da; } + .plc-never-connected { background-color: #fff3cd; } .api-section { margin-top: 30px; } .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; } .footer { margin-top: 40px; padding-top: 10px; border-top: 1px solid #ddd; color: #777; } + .doc-link { margin-top: 10px; padding: 10px; background-color: #e9f7fe; border-radius: 4px; } @@ -113,7 +110,16 @@ class APIServer: """ for plc_name, areas in summary.items(): - html += f"

PLC: {plc_name}

" + plc_status = self.cache_manager.plc_connection_status.get(plc_name, "unknown") + plc_class = "" + if plc_status == "connected": + plc_class = "plc-connected" + elif plc_status == "disconnected": + plc_class = "plc-disconnected" + else: + plc_class = "plc-never-connected" + + html += f'

PLC: {plc_name} (Status: {plc_status})

' html += """ @@ -121,6 +127,7 @@ class APIServer: + """ @@ -146,6 +153,7 @@ class APIServer: + """ @@ -186,6 +194,11 @@ class APIServer: + +
Type Size (bytes) StatusPLC Connection Last Update
{area['type']} {area['size']} {status_text}{area['plc_connection_status']} {area['last_update']}
+ + + + + + + + + + + + +
参数描述
plc_namePLC名称(如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

+
+ GET + /api/read//// +
+

从指定区域读取数据。

+ +

路径参数

+ + + + + + + + + + + + + + + + + + + + + +
参数描述
plc_namePLC名称(如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

+
+ POST + /api/write/// +
+

向指定区域写入数据。

+ +

路径参数

+ + + + + + + + + + + + + + + + + +
参数描述
plc_namePLC名称(如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_namestringPLC名称(与配置中一致)
area_namestring区域名称(与配置中一致)
offsetnumber起始偏移量(字节),默认为0
lengthnumber读取长度(字节),不提供则读取整个区域
+ +

请求示例

+
+ [ + { + "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_namestringPLC名称(与配置中一致)
area_namestring区域名称(与配置中一致)
offsetnumber起始偏移量(字节)
dataarray要写入的数据(字节数组)
+ +

请求示例

+
+ [ + { + "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

+
+ GET + /api/data// +
+

获取解析后的数据(如果配置了结构)。

+ +

路径参数

+ + + + + + + + + + + + + +
参数描述
plc_namePLC名称(如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): """