调整多字节写入接口,解决逻辑混淆的代码

This commit is contained in:
2025-08-14 10:10:38 +08:00
parent d3a38291a2
commit a9981fc84d
15 changed files with 754 additions and 413 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/gateway.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/gateway.iml" filepath="$PROJECT_DIR$/.idea/gateway.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,6 +6,7 @@ from functools import wraps
from config_manager import ConfigManager from config_manager import ConfigManager
import logging import logging
class APIServer: class APIServer:
"""REST API服务器提供PLC数据访问和配置管理功能""" """REST API服务器提供PLC数据访问和配置管理功能"""
@ -43,6 +44,7 @@ class APIServer:
def requires_auth(self, f): def requires_auth(self, f):
"""装饰器:需要认证的路由,保留函数元数据""" """装饰器:需要认证的路由,保留函数元数据"""
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
if not self.auth_enabled: if not self.auth_enabled:
@ -52,6 +54,7 @@ class APIServer:
if not auth or not self.check_auth(auth.username, auth.password): if not auth or not self.check_auth(auth.username, auth.password):
return self.authenticate() return self.authenticate()
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated return decorated
def get_summary(self): def get_summary(self):
@ -66,7 +69,8 @@ class APIServer:
summary[plc_name][area_name] = { summary[plc_name][area_name] = {
"status": area["status"], "status": area["status"],
"plc_connection_status": plc_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", "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"]
} }
@ -177,14 +181,21 @@ class APIServer:
</div> </div>
<div class="api-endpoint"> <div class="api-endpoint">
<strong>Single Read_Bool:</strong> GET /api/read_bool/<plc_name>/<area_name>/<offset>/<length><br> <strong>Single Data Write:</strong> POST /api/write_data/<plc_name>/<area_name>/<offset>/<data_type><br>
Example: /api/read_bool/PLC1/DB100_Read/0/2 Body: {"value": value}<br>
Example: POST /api/write_data/PLC1/DB100_Write/10/int with {"value": 12345}
</div> </div>
<div class="api-endpoint"> <div class="api-endpoint">
<strong>Single Write_Bool:</strong> POST /api/write_bool/<plc_name>/<area_name>/<offset><br> <strong>Array Write:</strong> POST /api/write_array/<plc_name>/<area_name>/<offset>/<data_type><br>
Body: Raw binary data<br> Body: {"values": [values]}<br>
Example: POST /api/write_bool/PLC1/DB100_Write/0 Example: POST /api/write_array/PLC1/DB100_Write/0/int with {"values": [1,2,3,4,5]}
</div>
<div class="api-endpoint">
<strong>Bit Write:</strong> POST /api/write_bit/<plc_name>/<area_name>/<byte_offset>/<bit_offset><br>
Body: 0 or 1<br>
Example: POST /api/write_bit/PLC1/DB100_Write/10/3 with 1
</div> </div>
<div class="api-endpoint"> <div class="api-endpoint">
@ -236,15 +247,17 @@ class APIServer:
for plc_name in self.cache_manager.plc_connection_status: for plc_name in self.cache_manager.plc_connection_status:
plc_statuses[plc_name] = { plc_statuses[plc_name] = {
"status": self.cache_manager.plc_connection_status[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])) "last_connected": time.strftime("%Y-%m-%d %H:%M:%S",
if self.cache_manager.plc_last_connected[plc_name] > 0 else "Never" time.localtime(self.cache_manager.plc_last_connected[plc_name]))
if self.cache_manager.plc_last_connected[plc_name] > 0 else "Never"
} }
return jsonify({ return jsonify({
"status": "running", "status": "running",
"start_time": self.start_time, "start_time": self.start_time,
"plc_count": len(self.config_manager.get_config().get("plcs", [])), "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 "plc_statuses": plc_statuses
}) })
@ -731,12 +744,12 @@ class APIServer:
</div> </div>
<div class="endpoint"> <div class="endpoint">
<h3>Single Write</h3> <h3>Single Write (Raw Bytes)</h3>
<div> <div>
<span class="method method-post">POST</span> <span class="method method-post">POST</span>
<code>/api/write/<plc_name>/<area_name>/<offset></code> <code>/api/write/<plc_name>/<area_name>/<offset></code>
</div> </div>
<p>向指定区域写入数据。</p> <p>向指定区域写入原始字节数据。</p>
<h4>路径参数</h4> <h4>路径参数</h4>
<table> <table>
@ -777,60 +790,12 @@ class APIServer:
</div> </div>
<div class="endpoint"> <div class="endpoint">
<h3>Single Read Bool</h3> <h3>Single Data Write</h3>
<div>
<span class="method method-get">GET</span>
<code>/api/read_bool/<plc_name>/<area_name>/<offset>/<length></code>
</div>
<p>从指定区域读取数据。</p>
<h4>路径参数</h4>
<table>
<tr>
<th>参数</th>
<th>描述</th>
</tr>
<tr>
<td>plc_name</td>
<td>PLC名称如PLC1</td>
</tr>
<tr>
<td>area_name</td>
<td>区域名称如DB100_Read</td>
</tr>
<tr>
<td>offset</td>
<td>起始偏移量(字节)</td>
</tr>
<tr>
<td>length</td>
<td>读取长度(字节)</td>
</tr>
</table>
<h4>响应示例</h4>
<div class="example">
{
"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"
}
</div>
</div>
<div class="endpoint">
<h3>Single Write Bool</h3>
<div> <div>
<span class="method method-post">POST</span> <span class="method method-post">POST</span>
<code>/api/write_bool/<plc_name>/<area_name>/<offset></code> <code>/api/write_data/<plc_name>/<area_name>/<offset>/<data_type></code>
</div> </div>
<p>向指定区域写入数据。</p> <p>以指定数据类型向指定区域写入单个数据。</p>
<h4>路径参数</h4> <h4>路径参数</h4>
<table> <table>
@ -848,12 +813,67 @@ class APIServer:
</tr> </tr>
<tr> <tr>
<td>offset</td> <td>offset</td>
<td>起始偏移量(字节)</td> <td>起始偏移量</td>
</tr>
<tr>
<td>data_type</td>
<td>数据类型bool/byte/int/dint/real/word/dword</td>
</tr> </tr>
</table> </table>
<h4>请求体</h4> <h4>请求体</h4>
<p>{0:True}</p> <p>JSON格式: {"value": 值}</p>
<h4>响应示例</h4>
<div class="example">
{
"status": "success",
"plc_name": "PLC1",
"area_name": "DB100_Write",
"offset": 10,
"data_type": "int",
"value": 12345,
"plc_connection_status": "connected",
"last_update": 1698754350.789,
"last_update_formatted": "2023-10-30 14:12:30"
}
</div>
</div>
<div class="endpoint">
<h3>Array Write</h3>
<div>
<span class="method method-post">POST</span>
<code>/api/write_array/<plc_name>/<area_name>/<offset>/<data_type></code>
</div>
<p>批量写入相同类型的数据到指定区域。</p>
<h4>路径参数</h4>
<table>
<tr>
<th>参数</th>
<th>描述</th>
</tr>
<tr>
<td>plc_name</td>
<td>PLC名称如PLC1</td>
</tr>
<tr>
<td>area_name</td>
<td>区域名称如DB100_Write</td>
</tr>
<tr>
<td>offset</td>
<td>起始偏移量</td>
</tr>
<tr>
<td>data_type</td>
<td>数据类型bool/byte/int/dint/real/word/dword</td>
</tr>
</table>
<h4>请求体</h4>
<p>JSON格式: {"values": [值1, 值2, ...]}</p>
<h4>响应示例</h4> <h4>响应示例</h4>
<div class="example"> <div class="example">
@ -862,7 +882,59 @@ class APIServer:
"plc_name": "PLC1", "plc_name": "PLC1",
"area_name": "DB100_Write", "area_name": "DB100_Write",
"offset": 0, "offset": 0,
"length": 1, "data_type": "int",
"count": 10,
"plc_connection_status": "connected",
"last_update": 1698754350.789,
"last_update_formatted": "2023-10-30 14:12:30"
}
</div>
</div>
<div class="endpoint">
<h3>Bit Write</h3>
<div>
<span class="method method-post">POST</span>
<code>/api/write_bit/<plc_name>/<area_name>/<byte_offset>/<bit_offset></code>
</div>
<p>向指定区域写入单个位。</p>
<h4>路径参数</h4>
<table>
<tr>
<th>参数</th>
<th>描述</th>
</tr>
<tr>
<td>plc_name</td>
<td>PLC名称如PLC1</td>
</tr>
<tr>
<td>area_name</td>
<td>区域名称如DB100_Write</td>
</tr>
<tr>
<td>byte_offset</td>
<td>字节偏移量</td>
</tr>
<tr>
<td>bit_offset</td>
<td>位偏移量0-7</td>
</tr>
</table>
<h4>请求体</h4>
<p>0或1表示False或True</p>
<h4>响应示例</h4>
<div class="example">
{
"status": "success",
"plc_name": "PLC1",
"area_name": "DB100_Write",
"byte_offset": 10,
"bit_offset": 3,
"value": 1,
"plc_connection_status": "connected", "plc_connection_status": "connected",
"last_update": 1698754350.789, "last_update": 1698754350.789,
"last_update_formatted": "2023-10-30 14:12:30" "last_update_formatted": "2023-10-30 14:12:30"
@ -1216,7 +1288,8 @@ class APIServer:
# 数据访问API # 数据访问API
# =========================== # ===========================
# 单个读取接口 # 单个读取接口
@self.app.route("/api/read/<plc_name>/<area_name>/<int:offset>/<int:length>", methods=["GET"], endpoint="single_read") @self.app.route("/api/read/<plc_name>/<area_name>/<int:offset>/<int:length>", methods=["GET"],
endpoint="single_read")
def single_read(plc_name, area_name, offset, length): def single_read(plc_name, area_name, offset, length):
"""从指定区域读取数据""" """从指定区域读取数据"""
data, error, plc_status, update_time = 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)
@ -1228,7 +1301,8 @@ class APIServer:
"message": error, "message": error,
"plc_connection_status": plc_status, "plc_connection_status": plc_status,
"last_update": update_time, "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" "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(update_time)) if update_time > 0 else "Never"
}), 400 }), 400
return jsonify({ return jsonify({
"status": "success", "status": "success",
@ -1242,40 +1316,12 @@ class APIServer:
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time))
}) })
# 单个读取BOOL类型接口 # 单个写入接口 (原始字节)
@self.app.route("/api/read_bool/<plc_name>/<area_name>/<int:offset>/<int:length>", 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/<plc_name>/<area_name>/<int:offset>", methods=["POST"], endpoint="single_write") @self.app.route("/api/write/<plc_name>/<area_name>/<int:offset>", methods=["POST"], endpoint="single_write")
def single_write(plc_name, area_name, offset): def single_write(plc_name, area_name, offset):
"""向指定区域写入数据""" """向指定区域写入原始字节数据"""
data = request.data data = request.data
if not data: if not data:
# 如果没有提供数据,返回错误
return jsonify({ return jsonify({
"status": "error", "status": "error",
"message": "No data provided", "message": "No data provided",
@ -1293,7 +1339,8 @@ class APIServer:
"message": error, "message": error,
"plc_connection_status": plc_status, "plc_connection_status": plc_status,
"last_update": update_time, "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" "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(update_time)) if update_time > 0 else "Never"
}), 400 }), 400
return jsonify({ return jsonify({
"status": "success", "status": "success",
@ -1306,22 +1353,50 @@ class APIServer:
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time))
}) })
# 单个写入BOOL类型接口 # 单个数据写入接口 (指定数据类型)
@self.app.route("/api/write_bool/<plc_name>/<area_name>/<int:offset>", methods=["POST"], endpoint="single_write_bool") @self.app.route("/api/write_data/<plc_name>/<area_name>/<int:offset>/<data_type>", methods=["POST"],
def single_write_bool(plc_name, area_name, offset): endpoint="write_data")
"""向指定区域写入数据""" @self.requires_auth
data = request.data def write_data(plc_name, area_name, offset, data_type):
if not data: """以指定数据类型写入单个数据"""
# 如果没有提供数据,返回错误 # 检查PLC是否存在
client = self.plc_manager.get_plc(plc_name)
if not client:
return jsonify({ return jsonify({
"status": "error", "status": "error",
"message": "No data provided", "message": "PLC not found",
"plc_connection_status": self.cache_manager.plc_connection_status.get(plc_name, "unknown"), "plc_name": plc_name
"last_update": 0, }), 404
"last_update_formatted": "N/A"
# 检查请求数据
if not request.is_json:
return jsonify({
"status": "error",
"message": "Request must be JSON (Content-Type: application/json)",
"plc_name": plc_name,
"area_name": area_name
}), 400 }), 400
success, error, plc_status, update_time = self.cache_manager.write_area_bool(plc_name, area_name, offset, data) json_data = request.json
if "value" not in json_data:
return jsonify({
"status": "error",
"message": "Missing 'value' field",
"plc_name": plc_name,
"area_name": area_name
}), 400
value = json_data["value"]
# 执行写入
success, error, plc_status, update_time = self.cache_manager.write_data(
plc_name,
area_name,
offset,
data_type,
value
)
if error: if error:
return jsonify({ return jsonify({
"status": "error", "status": "error",
@ -1333,12 +1408,150 @@ class APIServer:
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(update_time)) if update_time > 0 else "Never" time.localtime(update_time)) if update_time > 0 else "Never"
}), 400 }), 400
return jsonify({ return jsonify({
"status": "success", "status": "success",
"plc_name": plc_name, "plc_name": plc_name,
"area_name": area_name, "area_name": area_name,
"offset": offset, "offset": offset,
"length": 1, "data_type": data_type,
"value": value,
"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_array/<plc_name>/<area_name>/<int:offset>/<data_type>", methods=["POST"],
endpoint="write_array")
@self.requires_auth
def write_array(plc_name, area_name, offset, data_type):
"""批量写入相同类型的数据数组"""
# 检查PLC是否存在
client = self.plc_manager.get_plc(plc_name)
if not client:
return jsonify({
"status": "error",
"message": "PLC not found",
"plc_name": plc_name
}), 404
# 检查请求数据
if not request.is_json:
return jsonify({
"status": "error",
"message": "Request must be JSON (Content-Type: application/json)",
"plc_name": plc_name,
"area_name": area_name
}), 400
json_data = request.json
if "values" not in json_data or not isinstance(json_data["values"], list):
return jsonify({
"status": "error",
"message": "Missing or invalid 'values' array",
"plc_name": plc_name,
"area_name": area_name
}), 400
values = json_data["values"]
# 执行写入
success, error, plc_status, update_time = self.cache_manager.write_array(
plc_name,
area_name,
offset,
data_type,
values
)
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,
"data_type": data_type,
"count": len(values),
"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_bit/<plc_name>/<area_name>/<int:byte_offset>/<int:bit_offset>", methods=["POST"],
endpoint="write_bit")
@self.requires_auth
def write_bit(plc_name, area_name, byte_offset, bit_offset):
"""写入单个位"""
# 验证位偏移
if bit_offset < 0 or bit_offset > 7:
return jsonify({
"status": "error",
"message": "Bit offset must be between 0 and 7",
"plc_name": plc_name,
"area_name": area_name
}), 400
# 检查请求数据
data = request.data
if not data or len(data) == 0:
return jsonify({
"status": "error",
"message": "No value provided (use 0 or 1)",
"plc_name": plc_name,
"area_name": area_name
}), 400
# 解析值 (0或1)
value = int(data[0])
if value not in [0, 1]:
return jsonify({
"status": "error",
"message": "Value must be 0 or 1",
"plc_name": plc_name,
"area_name": area_name
}), 400
# 执行位写入
success, error, plc_status, update_time = self.cache_manager.write_bit(
plc_name,
area_name,
byte_offset,
bit_offset,
bool(value)
)
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,
"byte_offset": byte_offset,
"bit_offset": bit_offset,
"value": value,
"plc_connection_status": plc_status, "plc_connection_status": plc_status,
"last_update": update_time, "last_update": update_time,
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time))
@ -1359,7 +1572,7 @@ class APIServer:
"last_update_formatted": "N/A" "last_update_formatted": "N/A"
}), 400 }), 400
requests = request.get_json() requests = request.json
if not isinstance(requests, list): if not isinstance(requests, list):
return jsonify({ return jsonify({
"status": "error", "status": "error",
@ -1368,11 +1581,11 @@ class APIServer:
"last_update": 0, "last_update": 0,
"last_update_formatted": "N/A" "last_update_formatted": "N/A"
}), 400 }), 400
# 添加详细日志 # 添加详细日志
self.logger.info(f"Received batch read request: {json.dumps(requests)}") self.logger.info(f"Received batch read request: {json.dumps(requests)}")
results = self.cache_manager.batch_read(requests) results = self.cache_manager.batch_read(requests)
return jsonify(results) return jsonify(results)
except Exception as e: except Exception as e:
self.logger.error(f"Batch read error: {str(e)}", exc_info=True) self.logger.error(f"Batch read error: {str(e)}", exc_info=True)
@ -1422,43 +1635,6 @@ class APIServer:
"last_update_formatted": "N/A" "last_update_formatted": "N/A"
}), 500 }), 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/<plc_name>/<area_name>", methods=["GET"], endpoint="area_status") @self.app.route("/api/status/<plc_name>/<area_name>", methods=["GET"], endpoint="area_status")
def area_status(plc_name, area_name): def area_status(plc_name, area_name):

View File

@ -3,6 +3,7 @@ import time
import logging import logging
from snap7.util import * from snap7.util import *
class CacheManager: class CacheManager:
"""PLC数据缓存管理器""" """PLC数据缓存管理器"""
@ -91,7 +92,8 @@ class CacheManager:
self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name] self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name]
# 如果之前有数据,保留旧数据但标记状态 # 如果之前有数据,保留旧数据但标记状态
if self.last_update[plc_name][name] > 0: if self.last_update[plc_name][name] > 0:
self.logger.info(f"PLC {plc_name} area {name} disconnected but keeping last valid data") self.logger.info(
f"PLC {plc_name} area {name} disconnected but keeping last valid data")
except Exception as e: except Exception as e:
with self.lock: with self.lock:
self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name] self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name]
@ -160,7 +162,8 @@ class CacheManager:
summary[plc_name][area_name] = { summary[plc_name][area_name] = {
"status": area_status, "status": area_status,
"plc_connection_status": plc_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", "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"]
} }
@ -187,7 +190,8 @@ class CacheManager:
"status": area_status, "status": area_status,
"plc_connection_status": plc_status, "plc_connection_status": plc_status,
"last_update": last_update, "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", "last_update_formatted": 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"]
} }
@ -502,7 +506,8 @@ class CacheManager:
"status": "error", "status": "error",
"plc_connection_status": plc_status, "plc_connection_status": plc_status,
"last_update": update_time, "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", "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(update_time)) if update_time > 0 else "Never",
"message": error "message": error
}) })
else: else:
@ -552,7 +557,8 @@ class CacheManager:
"status": "error", "status": "error",
"plc_connection_status": plc_status, "plc_connection_status": plc_status,
"last_update": update_time, "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", "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(update_time)) if update_time > 0 else "Never",
"message": error, "message": error,
"offset": offset "offset": offset
}) })
@ -603,7 +609,8 @@ class CacheManager:
"status": "error", "status": "error",
"plc_connection_status": plc_status, "plc_connection_status": plc_status,
"last_update": update_time, "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", "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(update_time)) if update_time > 0 else "Never",
"message": error, "message": error,
"offset": offset "offset": offset
}) })
@ -645,7 +652,8 @@ class CacheManager:
parsed["status"] = area_status parsed["status"] = area_status
parsed["plc_connection_status"] = plc_status parsed["plc_connection_status"] = plc_status
parsed["last_update"] = last_update 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" parsed["last_update_formatted"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(
last_update)) if last_update > 0 else "Never"
return parsed return parsed
else: else:
return { return {
@ -653,5 +661,6 @@ class CacheManager:
"status": area_status, "status": area_status,
"plc_connection_status": plc_status, "plc_connection_status": plc_status,
"last_update": last_update, "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" "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(last_update)) if last_update > 0 else "Never"
} }

View File

@ -3,7 +3,8 @@ import logging
from threading import Lock from threading import Lock
import time import time
from snap7.util import * from snap7.util import *
import ast import struct
import json
class Snap7Client: class Snap7Client:
"""Snap7客户端处理与PLC的通信""" """Snap7客户端处理与PLC的通信"""
@ -140,23 +141,24 @@ class Snap7Client:
def write_db(self, db_number, offset, data): def write_db(self, db_number, offset, data):
""" """
向DB块写入数据 向DB块写入原始字节数据
Args: Args:
db_number: DB编号 db_number: DB编号
offset: 起始偏移量 offset: 起始偏移量
data: 要写入的数据 data: 字节序列 (bytes, bytearray 或 list of int)
Returns: Returns:
bool: 是否写入成功 bool: 是否写入成功
""" """
values = int(data) # 确保data是字节序列
print("values:", values) if isinstance(data, list):
value = bytearray(0) data = bytes(data)
if isinstance(values, int): elif isinstance(data, int):
set_int(value, offset, values) data = bytes([data])
data = value elif not isinstance(data, (bytes, bytearray)):
print(data) self.logger.error("Data must be bytes, bytearray or list of integers")
return False
if not self.connected and not self.connect(): if not self.connected and not self.connect():
self.logger.warning(f"Write failed: not connected to {self.ip}") self.logger.warning(f"Write failed: not connected to {self.ip}")
@ -172,90 +174,204 @@ class Snap7Client:
self.connected = False self.connected = False
return False return False
def batch_write_db(self, db_number, offset, data):
def write_bit(self, db_number, byte_offset, bit_offset, value):
""" """
DB块写入数据 写入DB块中的单个位
Args: Args:
db_number: DB编号 db_number: DB编号
offset: 起始偏移量 byte_offset: 字节偏移量
data: 要写入的数据 bit_offset: 位偏移量 (0-7)
value: 布尔值 (True/False)
Returns: Returns:
bool: 是否写入成功 bool: 是否写入成功
""" """
if not self.connected and not self.connect(): # 验证位偏移量
self.logger.warning(f"Write failed: not connected to {self.ip}") if bit_offset < 0 or bit_offset > 7:
return False raise ValueError("Bit offset must be between 0 and 7")
try: # 读取包含目标位的字节
with self.lock: byte_data = self.read_db(db_number, byte_offset, 1)
self.client.db_write(db_number, offset, data) if not byte_data or len(byte_data) != 1:
self.logger.debug(f"Wrote {len(data)} bytes to DB{db_number} offset {offset}") self.logger.error(f"Failed to read byte at offset {byte_offset} for bit write")
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): # 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 write_data(self, db_number, offset, data_type, value):
""" """
向DB块写入数据 以指定数据类型向DB块写入单个数据
Args: Args:
db_number: DB编号 db_number: DB编号
offset: 起始偏移量 offset: 起始偏移量
data: 要写入的bool类型数据 data_type: 数据类型 ('bool', 'byte', 'int', 'dint', 'real', 'word', 'dword')
value: 要写入的值
Returns: Returns:
bool: 是否写入成功 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: try:
with self.lock: if data_type == "bool":
print(db_number, offset, data) # bool类型需要特殊处理位写入
self.client.db_write(db_number, offset, data) byte_offset = offset // 8
self.logger.debug(f"Wrote {len(data)} bytes to DB{db_number} offset {offset}") bit_offset = offset % 8
return True return self.write_bit(db_number, byte_offset, bit_offset, value)
elif data_type == "byte":
# 单字节
return self.write_db(db_number, offset, [int(value) & 0xFF])
elif data_type == "int":
# 有符号16位整数 (2字节)
packed = struct.pack(">h", int(value))
return self.write_db(db_number, offset, packed)
elif data_type == "dint":
# 有符号32位整数 (4字节)
packed = struct.pack(">l", int(value))
return self.write_db(db_number, offset, packed)
elif data_type == "real":
# 32位浮点数 (4字节)
packed = struct.pack(">f", float(value))
return self.write_db(db_number, offset, packed)
elif data_type == "word":
# 无符号16位整数 (2字节)
packed = struct.pack(">H", int(value) & 0xFFFF)
return self.write_db(db_number, offset, packed)
elif data_type == "dword":
# 无符号32位整数 (4字节)
packed = struct.pack(">I", int(value) & 0xFFFFFFFF)
return self.write_db(db_number, offset, packed)
else:
self.logger.error(f"Unsupported data type: {data_type}")
return False
except Exception as e: except Exception as e:
self.logger.error(f"Write DB{db_number} error: {e}") self.logger.error(f"Error converting {data_type} value {value}: {e}")
self.connected = False
return False return False
def batch_write_db_bool(self, db_number, offset, data): def write_array(self, db_number, offset, data_type, values):
""" """
向DB块写入数据 批量写入相同类型的数据数组
Args: Args:
db_number: DB编号 db_number: DB编号
offset: 起始偏移量 offset: 起始偏移量
data: 要写入的bool类型数据 data_type: 数据类型 ('bool', 'byte', 'int', 'dint', 'real', 'word', 'dword')
values: 要写入的值列表
Returns: Returns:
bool: 是否写入成功 bool: 是否写入成功
""" """
if not self.connected and not self.connect():
self.logger.warning(f"Write failed: not connected to {self.ip}")
return False
try: try:
with self.lock: # 检查是否是支持的数据类型
print(db_number, offset, data) if data_type not in ["bool", "byte", "int", "dint", "real", "word", "dword"]:
self.client.db_write(db_number, offset, data) self.logger.error(f"Unsupported data type for array write: {data_type}")
self.logger.debug(f"Wrote {len(data)} bytes to DB{db_number} offset {offset}") return False
# 对于bool类型需要特殊处理
if data_type == "bool":
# 逐个写入bool值
for i, value in enumerate(values):
if not self.write_data(db_number, offset + i, "bool", value):
return False
return True return True
# 计算总字节数
if data_type in ["int", "word"]:
item_size = 2
elif data_type in ["dint", "real", "dword"]:
item_size = 4
else: # byte
item_size = 1
total_size = item_size * len(values)
# 打包所有数据
packed_data = b''
for value in values:
if data_type == "byte":
packed_data += struct.pack("B", int(value) & 0xFF)
elif data_type == "int":
packed_data += struct.pack(">h", int(value))
elif data_type == "dint":
packed_data += struct.pack(">l", int(value))
elif data_type == "real":
packed_data += struct.pack(">f", float(value))
elif data_type == "word":
packed_data += struct.pack(">H", int(value) & 0xFFFF)
elif data_type == "dword":
packed_data += struct.pack(">I", int(value) & 0xFFFFFFFF)
# 一次性写入所有数据
return self.write_db(db_number, offset, packed_data)
except Exception as e: except Exception as e:
self.logger.error(f"Write DB{db_number} error: {e}") self.logger.error(f"Error writing {data_type} array: {e}")
self.connected = False
return False return False