From d3a38291a295fde9229b79a9ebc5e580d2a4f164 Mon Sep 17 00:00:00 2001 From: chuyiwen Date: Wed, 13 Aug 2025 23:55:46 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=BB=E6=AD=A3=E5=B8=B8=EF=BC=8C=E5=A4=9A?= =?UTF-8?q?=E5=AD=97=E8=8A=82=E5=86=99=E5=85=A5=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.json | 23 +- config/config.json.bak | 21 +- .../__pycache__/api_server.cpython-313.pyc | Bin 59312 -> 0 bytes .../__pycache__/cache_manager.cpython-313.pyc | Bin 18779 -> 0 bytes .../__pycache__/config_loader.cpython-313.pyc | Bin 733 -> 0 bytes .../config_manager.cpython-313.pyc | Bin 5440 -> 0 bytes .../config_validator.cpython-313.pyc | Bin 2501 -> 0 bytes .../__pycache__/plc_manager.cpython-313.pyc | Bin 2081 -> 0 bytes .../__pycache__/snap7_client.cpython-313.pyc | Bin 6582 -> 0 bytes gateway/api_server.py | 215 ++++++++- gateway/cache_manager.py | 453 +++++++++++++----- gateway/snap7_client.py | 147 +++++- 12 files changed, 700 insertions(+), 159 deletions(-) delete mode 100644 gateway/__pycache__/api_server.cpython-313.pyc delete mode 100644 gateway/__pycache__/cache_manager.cpython-313.pyc delete mode 100644 gateway/__pycache__/config_loader.cpython-313.pyc delete mode 100644 gateway/__pycache__/config_manager.cpython-313.pyc delete mode 100644 gateway/__pycache__/config_validator.cpython-313.pyc delete mode 100644 gateway/__pycache__/plc_manager.cpython-313.pyc delete mode 100644 gateway/__pycache__/snap7_client.cpython-313.pyc diff --git a/config/config.json b/config/config.json index fb847c4..4a90a1c 100644 --- a/config/config.json +++ b/config/config.json @@ -2,7 +2,7 @@ "plcs": [ { "name": "PLC1", - "ip": "192.168.0.100", + "ip": "192.168.0.1", "rack": 0, "slot": 1, "areas": [ @@ -11,7 +11,7 @@ "type": "read", "db_number": 100, "offset": 0, - "size": 4000, + "size": 5000, "structure": [ { "name": "temperature", @@ -35,7 +35,7 @@ "name": "DB100_Write", "type": "write", "db_number": 100, - "offset": 4000, + "offset": 0, "size": 5000 }, { @@ -43,22 +43,7 @@ "type": "read_write", "db_number": 202, "offset": 0, - "size": 2000 - } - ] - }, - { - "name": "PLC2", - "ip": "192.168.0.101", - "rack": 0, - "slot": 1, - "areas": [ - { - "name": "DB100_Read", - "type": "read", - "db_number": 100, - "offset": 0, - "size": 4000 + "size": 816 } ] } diff --git a/config/config.json.bak b/config/config.json.bak index fb847c4..6fd526b 100644 --- a/config/config.json.bak +++ b/config/config.json.bak @@ -2,7 +2,7 @@ "plcs": [ { "name": "PLC1", - "ip": "192.168.0.100", + "ip": "192.168.0.1", "rack": 0, "slot": 1, "areas": [ @@ -11,7 +11,7 @@ "type": "read", "db_number": 100, "offset": 0, - "size": 4000, + "size": 5000, "structure": [ { "name": "temperature", @@ -43,22 +43,7 @@ "type": "read_write", "db_number": 202, "offset": 0, - "size": 2000 - } - ] - }, - { - "name": "PLC2", - "ip": "192.168.0.101", - "rack": 0, - "slot": 1, - "areas": [ - { - "name": "DB100_Read", - "type": "read", - "db_number": 100, - "offset": 0, - "size": 4000 + "size": 816 } ] } diff --git a/gateway/__pycache__/api_server.cpython-313.pyc b/gateway/__pycache__/api_server.cpython-313.pyc deleted file mode 100644 index 260078de010e4939e7624dcab1220eb2b183c0b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59312 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 BtVI9- diff --git a/gateway/__pycache__/config_loader.cpython-313.pyc b/gateway/__pycache__/config_loader.cpython-313.pyc deleted file mode 100644 index ce1d239294714a10d84a302ac7d84f57783fa7fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 733 zcmYjP&ubGw6n?Y2CQa6)rh*5R?pUN4tPv@tP&8Q7UJ9vX$ti^rlHD{h?u6M5n&Lqa z(N-j;!Q6Stp7nop)l=1F#hOG2u1MZn`AfoX6C(b-n{qCyqW38Vo^YO^Xk!h ziU53-lRy+7^d}K)g9#>C20pZe6H_&n(@-T`DU+mjD1$RaQ4b{> zFb58_*<=4re3*kdpYMY2Da8Tu;ULl^If%oo5=2=f9|_iSU@CL|J_TJomDOvi0XE)1V*>fUZSDN*bPgXs|IvB< z{l$~sySE#YH<)8r$_-xdD$JpFrDjowdDLbNr?fx?;hAJuD9jaFz2L2jkafS}dag(; zVPuAROET^j=Zp)D)h$P8RhOZzu|g@S2)#I>58_65>iX`P z-B_1qKE+qMV=KK#?CG8M?B|HFbLC+CWUIN=Y$u*Q_^m>e?nMmje_S2;-k(}D)DK2< z@lrTRHYSE!ULKm1EG(&V`BYr~hrv-^LLVV;e0mULSMRb?qh_VKylweA+!LsNsf3Vy P(Dxy7q=m>?o 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 G`CkDEvabyQ diff --git a/gateway/api_server.py b/gateway/api_server.py index cf1d149..a70941e 100644 --- a/gateway/api_server.py +++ b/gateway/api_server.py @@ -176,6 +176,17 @@ class APIServer: Example: POST /api/write/PLC1/DB100_Write/10 with 4 bytes of data +

+ Single Read_Bool: GET /api/read_bool////
+ Example: /api/read_bool/PLC1/DB100_Read/0/2 +
+ +
+ Single Write_Bool: POST /api/write_bool///
+ Body: Raw binary data
+ Example: POST /api/write_bool/PLC1/DB100_Write/0 +
+
Batch Read: POST /api/batch_read
Body: JSON array of read requests
@@ -764,6 +775,100 @@ class APIServer: }
+ +
+

Single Read Bool

+
+ GET + /api/read_bool//// +
+

从指定区域读取数据。

+ +

路径参数

+ + + + + + + + + + + + + + + + + + + + + +
参数描述
plc_namePLC名称(如PLC1)
area_name区域名称(如DB100_Read)
offset起始偏移量(字节)
length读取长度(字节)
+ +

响应示例

+
+ { + "status": "success", + "plc_name": "PLC1", + "area_name": "DB100_Read", + "offset": 0, + "length": 2, + "data": [0:False, 1:False], + "plc_connection_status": "connected", + "last_update": 1698754321.456, + "last_update_formatted": "2023-10-30 14:12:01" + } +
+
+ +
+

Single Write Bool

+
+ POST + /api/write_bool/// +
+

向指定区域写入数据。

+ +

路径参数

+ + + + + + + + + + + + + + + + + +
参数描述
plc_namePLC名称(如PLC1)
area_name区域名称(如DB100_Write)
offset起始偏移量(字节)
+ +

请求体

+

{0:True}

+ +

响应示例

+
+ { + "status": "success", + "plc_name": "PLC1", + "area_name": "DB100_Write", + "offset": 0, + "length": 1, + "plc_connection_status": "connected", + "last_update": 1698754350.789, + "last_update_formatted": "2023-10-30 14:12:30" + } +
+

Batch Read

@@ -1137,6 +1242,33 @@ class APIServer: "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) }) + # 单个读取BOOL类型接口 + @self.app.route("/api/read_bool////", methods=["GET"], endpoint="single_read_bool") + def single_read_bool(plc_name, area_name, offset, length): + """从指定区域读取数据""" + data, error, plc_status, update_time = self.cache_manager.read_area_bool(plc_name, area_name, offset, length) + 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, + "length": length, + "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)) + }) + # 单个写入接口 @self.app.route("/api/write///", methods=["POST"], endpoint="single_write") def single_write(plc_name, area_name, offset): @@ -1174,6 +1306,44 @@ class APIServer: "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) }) + # 单个写入BOOL类型接口 + @self.app.route("/api/write_bool///", methods=["POST"], endpoint="single_write_bool") + def single_write_bool(plc_name, area_name, offset): + """向指定区域写入数据""" + data = request.data + if not data: + # 如果没有提供数据,返回错误 + 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, plc_status, update_time = self.cache_manager.write_area_bool(plc_name, area_name, offset, data) + 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, + "length": 1, + "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_read", methods=["POST"], endpoint="batch_read") def batch_read(): @@ -1188,8 +1358,8 @@ class APIServer: "last_update": 0, "last_update_formatted": "N/A" }), 400 - - requests = request.json + + requests = request.get_json() if not isinstance(requests, list): return jsonify({ "status": "error", @@ -1198,11 +1368,11 @@ class APIServer: "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: self.logger.error(f"Batch read error: {str(e)}", exc_info=True) @@ -1252,6 +1422,43 @@ class APIServer: "last_update_formatted": "N/A" }), 500 + @self.app.route("/api/batch_write_bool", methods=["POST"], endpoint="batch_write_bool") + def batch_write_bool(): + """批量写入多个区域的数据""" + 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", + "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_bool(requests) + return jsonify(results) + except Exception as e: + 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): diff --git a/gateway/cache_manager.py b/gateway/cache_manager.py index 2ff30c6..48adf16 100644 --- a/gateway/cache_manager.py +++ b/gateway/cache_manager.py @@ -1,6 +1,7 @@ import threading import time import logging +from snap7.util import * class CacheManager: """PLC数据缓存管理器""" @@ -197,7 +198,7 @@ class CacheManager: area = self.cache.get(plc_name, {}).get(area_name) if not area: return None, "Area not found", "unknown", 0 - + if offset + length > area["size"]: return None, "Offset out of bounds", "unknown", 0 @@ -227,6 +228,45 @@ 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 read_area_bool(self, plc_name, area_name, offset, length): + """单个区域读取""" + with self.lock: + area = self.cache.get(plc_name, {}).get(area_name) + 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_bool(area["db_number"], area["offset"] + offset, length) + # 验证数据有效性 + if all(isinstance(val, bool) for val in data.values()): + # 按字典键顺序更新多个值 + for i, val in data.items(): + area["data"][offset + i] = val # 确保offset+i不越界 + + # area["data"][offset] = data.values + 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 + return None, "Invalid data returned", plc_status, 0 + 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_area(self, plc_name, area_name, offset, data): """单个区域写入""" @@ -251,6 +291,7 @@ class CacheManager: return False, f"PLC not connected (status: {plc_status})", plc_status, 0 try: + print(data) success = client.write_db(area["db_number"], area["offset"] + offset, data) if success: # 更新缓存中的这部分数据 @@ -268,125 +309,315 @@ 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_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: + print("data:", data) + for i, byte in enumerate(data): + print("i,byte:", i, byte) + byte_data = bytes([byte]) + current_offset = offset + (i * 2) + byte_value = byte_data[0] + + value = bytearray(2) + if isinstance(byte_value, int): + set_int(value, 0, byte_value) + data = value + + print(area["db_number"], current_offset, data) + success = client.batch_write_db(area["db_number"], current_offset, data) + if success: + # 更新缓存中的这部分数据 + for j in range(len(data)): + area["data"][offset + j] = data[j] + update_time = time.time() + self.last_update[plc_name][area_name] = update_time + area["status"] = "connected (last write)" + else: + area["status"] = plc_status + return False, "Write failed", plc_status, 0 + return True, None, plc_status, update_time + 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 batch_write_bool_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: + value = bytearray(offset + 1) + for bit, bit_value in enumerate(data): + print("i,byte:", bit, bit_value) + set_bool(value, offset, bit, bit_value) + data = value + + print(area["db_number"], offset, data) + success = client.batch_write_db_bool(area["db_number"], offset, data) + if success: + # 更新缓存中的这部分数据 + for j in range(len(data)): + area["data"][offset + j] = data[j] + update_time = time.time() + self.last_update[plc_name][area_name] = update_time + area["status"] = "connected (last write)" + else: + area["status"] = plc_status + return False, "Write failed", plc_status, 0 + return True, None, plc_status, update_time + 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 write_area_bool(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_bool(area["db_number"], area["offset"] + offset, data) + if success: + # 更新缓存中的这部分数据 + for i in range(len(data)): + area["data"][offset + i] = data[i] + 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 batch_read(self, requests): """批量读取""" results = [] - 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) - - # 获取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) - }) + + for req in requests: + plc_name = req["plc_name"] + area_name = req["area_name"] + offset = req.get("offset", 0) + length = req.get("length", None) + + # 获取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 = [] - 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) - }) + 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.batch_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 batch_write_bool(self, requests): + """批量写入""" + results = [] + 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.batch_write_bool_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): diff --git a/gateway/snap7_client.py b/gateway/snap7_client.py index 2c7a7fd..464f50f 100644 --- a/gateway/snap7_client.py +++ b/gateway/snap7_client.py @@ -2,6 +2,8 @@ import snap7 import logging from threading import Lock import time +from snap7.util import * +import ast class Snap7Client: """Snap7客户端,处理与PLC的通信""" @@ -34,8 +36,8 @@ class Snap7Client: try: # 尝试读取PLC的运行状态 cpu_state = self.client.get_cpu_state() - return cpu_state in [snap7.snap7types.cpu_statuses['S7CpuStatusRun'], - snap7.snap7types.cpu_statuses['S7CpuStatusStop']] + print("当前 CPU 状态:", cpu_state) + return cpu_state in ['S7CpuStatusRun', 'S7CpuStatusStop'] except: return False @@ -51,6 +53,7 @@ class Snap7Client: self.last_connect_attempt = current_time try: self.client.connect(self.ip, self.rack, self.slot) + # 验证连接是否真正有效 if self.client.get_connected() and self.is_valid_connection(): self.connected = True @@ -88,7 +91,7 @@ class Snap7Client: return None # 返回None而不是零填充数据 try: - with self.lock: + with self.lock: # 进入锁,其他线程需等待 data = self.client.db_read(db_number, offset, size) # 验证返回数据的有效性 if data is None or len(data) != size: @@ -101,27 +104,157 @@ class Snap7Client: self.connected = False return None + def read_db_bool(self, db_number, offset, bit_length): + """ + 从 DB 块中读取一个字节,并提取其中的多个 BOOL 位 + + Args: + db_number (int): DB块编号 + offset (int): 要读取的字节偏移地址 + bit_length: 第几位,如1,表示第1位 + + Returns: + result:返回位值 + """ + 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, 1) + result = {} + + for bit in range(bit_length): + result[bit] = bool(data[0] & (1 << bit)) + + if result is None or len(result) != bit_length: + self.connected = False + self.logger.error(f"Read DB{db_number} returned invalid data size (expected {bit_length}, got {len(result) if data else 0})") + return None + return result + except Exception as e: + self.logger.error(f"Read DB{db_number} error: {e}") + self.connected = False + return None + def write_db(self, db_number, offset, data): """ 向DB块写入数据 - + Args: db_number: DB编号 offset: 起始偏移量 - 要写入的数据 - + data: 要写入的数据 + Returns: bool: 是否写入成功 """ + values = int(data) + print("values:", values) + value = bytearray(0) + if isinstance(values, int): + set_int(value, offset, values) + data = value + print(data) + if not self.connected and not self.connect(): self.logger.warning(f"Write failed: not connected to {self.ip}") return False - + try: with self.lock: 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 + return False + + def batch_write_db(self, db_number, offset, data): + """ + 向DB块写入数据 + + Args: + db_number: DB编号 + offset: 起始偏移量 + data: 要写入的数据 + + Returns: + bool: 是否写入成功 + """ + if not self.connected and not self.connect(): + self.logger.warning(f"Write failed: not connected to {self.ip}") + return False + + try: + with self.lock: + 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 + return False + + def write_db_bool(self, db_number, offset, data): + """ + 向DB块写入数据 + + Args: + db_number: DB编号 + offset: 起始偏移量 + data: 要写入的bool类型数据 + + Returns: + bool: 是否写入成功 + """ + data = data.decode('utf-8') + # 将字符串安全转换为字典 + data_dict = ast.literal_eval(data) # 输出: {0: True} + # value = bytearray(1) + value = bytearray(offset+1) + for bit, val in data_dict.items(): + set_bool(value, offset, bit, val) + data = value + if not self.connected and not self.connect(): + self.logger.warning(f"Write failed: not connected to {self.ip}") + return False + + try: + with self.lock: + print(db_number, offset, data) + 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 + return False + + def batch_write_db_bool(self, db_number, offset, data): + """ + 向DB块写入数据 + + Args: + db_number: DB编号 + offset: 起始偏移量 + data: 要写入的bool类型数据 + + Returns: + bool: 是否写入成功 + """ + if not self.connected and not self.connect(): + self.logger.warning(f"Write failed: not connected to {self.ip}") + return False + + try: + with self.lock: + print(db_number, offset, data) + 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