测试所有接口,修复连接状态,新增接口文档
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -113,7 +110,16 @@ class APIServer:
|
||||
"""
|
||||
|
||||
for plc_name, areas in summary.items():
|
||||
html += f"<h2>PLC: {plc_name}</h2>"
|
||||
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'<h2 class="{plc_class}">PLC: {plc_name} (Status: {plc_status})</h2>'
|
||||
html += """
|
||||
<table>
|
||||
<tr>
|
||||
@ -121,6 +127,7 @@ class APIServer:
|
||||
<th>Type</th>
|
||||
<th>Size (bytes)</th>
|
||||
<th>Status</th>
|
||||
<th>PLC Connection</th>
|
||||
<th>Last Update</th>
|
||||
</tr>
|
||||
"""
|
||||
@ -146,6 +153,7 @@ class APIServer:
|
||||
<td>{area['type']}</td>
|
||||
<td>{area['size']}</td>
|
||||
<td class="{status_class}">{status_text}</td>
|
||||
<td>{area['plc_connection_status']}</td>
|
||||
<td>{area['last_update']}</td>
|
||||
</tr>
|
||||
"""
|
||||
@ -186,6 +194,11 @@ class APIServer:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="doc-link">
|
||||
<h2>API Documentation</h2>
|
||||
<p><a href="/api/doc">View detailed API documentation</a> - Full description of all API endpoints</p>
|
||||
</div>
|
||||
|
||||
<div class="config-link">
|
||||
<h2>Configuration</h2>
|
||||
<p><a href="/config">Edit configuration</a> - Modify PLC connections and data areas</p>
|
||||
@ -208,11 +221,20 @@ class APIServer:
|
||||
@self.app.route("/api/status", endpoint="system_status")
|
||||
def system_status():
|
||||
"""获取系统状态信息"""
|
||||
plc_statuses = {}
|
||||
for plc_name in self.cache_manager.plc_connection_status:
|
||||
plc_statuses[plc_name] = {
|
||||
"status": self.cache_manager.plc_connection_status[plc_name],
|
||||
"last_connected": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.cache_manager.plc_last_connected[plc_name]))
|
||||
if self.cache_manager.plc_last_connected[plc_name] > 0 else "Never"
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"status": "running",
|
||||
"start_time": self.start_time,
|
||||
"plc_count": len(self.config_manager.get_config().get("plcs", [])),
|
||||
"cache_size": sum(len(area["data"]) for plc in self.cache_manager.cache.values() for area in plc.values())
|
||||
"cache_size": sum(len(area["data"]) for plc in self.cache_manager.cache.values() for area in plc.values()),
|
||||
"plc_statuses": plc_statuses
|
||||
})
|
||||
|
||||
# ===========================
|
||||
@ -279,10 +301,21 @@ class APIServer:
|
||||
.config-help h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.doc-link {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #e9f7fe;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PLC Gateway Configuration</h1>
|
||||
|
||||
<div class="doc-link">
|
||||
<p><a href="/api/doc">View API documentation</a> - Learn how to use the API</p>
|
||||
</div>
|
||||
|
||||
<div class="config-container">
|
||||
<p>Edit the configuration JSON below. Be careful with the syntax.</p>
|
||||
|
||||
@ -488,6 +521,592 @@ class APIServer:
|
||||
"message": f"Error saving config: {str(e)}"
|
||||
}), 500
|
||||
|
||||
# ===========================
|
||||
# 新增 API 文档接口
|
||||
# ===========================
|
||||
@self.app.route("/api/doc", endpoint="api_doc")
|
||||
def api_doc():
|
||||
"""API文档页面"""
|
||||
html = """
|
||||
<html>
|
||||
<head>
|
||||
<title>PLC Gateway API Documentation</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
h1, h2, h3 { color: #2c3e50; }
|
||||
.endpoint {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #3498db;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.method {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.method-get { background-color: #27ae60; }
|
||||
.method-post { background-color: #3498db; }
|
||||
.method-put { background-color: #f39c12; }
|
||||
.method-delete { background-color: #e74c3c; }
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 15px 0;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
code {
|
||||
background-color: #f5f5f5;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.example {
|
||||
background-color: #f9f9f9;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
.nav {
|
||||
background-color: #e9f7fe;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.nav a {
|
||||
margin-right: 15px;
|
||||
text-decoration: none;
|
||||
color: #3498db;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PLC Gateway API Documentation</h1>
|
||||
|
||||
<div class="nav">
|
||||
<a href="#status-api">Status API</a> |
|
||||
<a href="#data-api">Data API</a> |
|
||||
<a href="#config-api">Configuration API</a>
|
||||
</div>
|
||||
|
||||
<h2 id="status-api">Status API</h2>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>System Status</h3>
|
||||
<div>
|
||||
<span class="method method-get">GET</span>
|
||||
<code>/api/status</code>
|
||||
</div>
|
||||
<p>获取系统状态信息,包括启动时间、PLC数量和缓存大小。</p>
|
||||
|
||||
<h4>响应示例</h4>
|
||||
<div class="example">
|
||||
{
|
||||
"status": "running",
|
||||
"start_time": "2023-10-30 14:30:22",
|
||||
"plc_count": 2,
|
||||
"cache_size": 11000,
|
||||
"plc_statuses": {
|
||||
"PLC1": {
|
||||
"status": "connected",
|
||||
"last_connected": "2023-10-30 14:35:10"
|
||||
},
|
||||
"PLC2": {
|
||||
"status": "disconnected",
|
||||
"last_connected": "Never"
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Area Status</h3>
|
||||
<div>
|
||||
<span class="method method-get">GET</span>
|
||||
<code>/api/status/<plc_name>/<area_name></code>
|
||||
</div>
|
||||
<p>获取指定PLC区域的状态信息。</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>
|
||||
</table>
|
||||
|
||||
<h4>响应示例</h4>
|
||||
<div class="example">
|
||||
{
|
||||
"status": "connected",
|
||||
"plc_connection_status": "connected",
|
||||
"last_update": 1698754321.456,
|
||||
"last_update_formatted": "2023-10-30 14:12:01",
|
||||
"size": 4000,
|
||||
"type": "read"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 id="data-api">Data API</h2>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Single Read</h3>
|
||||
<div>
|
||||
<span class="method method-get">GET</span>
|
||||
<code>/api/read/<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": 4,
|
||||
"data": [0, 0, 123, 45],
|
||||
"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</h3>
|
||||
<div>
|
||||
<span class="method method-post">POST</span>
|
||||
<code>/api/write/<plc_name>/<area_name>/<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>offset</td>
|
||||
<td>起始偏移量(字节)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4>请求体</h4>
|
||||
<p>原始二进制数据</p>
|
||||
|
||||
<h4>响应示例</h4>
|
||||
<div class="example">
|
||||
{
|
||||
"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"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Batch Read</h3>
|
||||
<div>
|
||||
<span class="method method-post">POST</span>
|
||||
<code>/api/batch_read</code>
|
||||
</div>
|
||||
<p>批量读取多个区域的数据。</p>
|
||||
|
||||
<h4>请求体</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>字段</th>
|
||||
<th>类型</th>
|
||||
<th>必需</th>
|
||||
<th>描述</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>plc_name</td>
|
||||
<td>string</td>
|
||||
<td>是</td>
|
||||
<td>PLC名称(与配置中一致)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>area_name</td>
|
||||
<td>string</td>
|
||||
<td>是</td>
|
||||
<td>区域名称(与配置中一致)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>offset</td>
|
||||
<td>number</td>
|
||||
<td>否</td>
|
||||
<td>起始偏移量(字节),默认为0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>length</td>
|
||||
<td>number</td>
|
||||
<td>否</td>
|
||||
<td>读取长度(字节),不提供则读取整个区域</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4>请求示例</h4>
|
||||
<div class="example">
|
||||
[
|
||||
{
|
||||
"plc_name": "PLC1",
|
||||
"area_name": "DB100_Read",
|
||||
"offset": 0,
|
||||
"length": 4
|
||||
},
|
||||
{
|
||||
"plc_name": "PLC1",
|
||||
"area_name": "DB202_Params",
|
||||
"offset": 10,
|
||||
"length": 2
|
||||
}
|
||||
]
|
||||
</div>
|
||||
|
||||
<h4>响应示例</h4>
|
||||
<div class="example">
|
||||
[
|
||||
{
|
||||
"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]
|
||||
}
|
||||
]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Batch Write</h3>
|
||||
<div>
|
||||
<span class="method method-post">POST</span>
|
||||
<code>/api/batch_write</code>
|
||||
</div>
|
||||
<p>批量写入多个区域的数据。</p>
|
||||
|
||||
<h4>请求体</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>字段</th>
|
||||
<th>类型</th>
|
||||
<th>必需</th>
|
||||
<th>描述</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>plc_name</td>
|
||||
<td>string</td>
|
||||
<td>是</td>
|
||||
<td>PLC名称(与配置中一致)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>area_name</td>
|
||||
<td>string</td>
|
||||
<td>是</td>
|
||||
<td>区域名称(与配置中一致)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>offset</td>
|
||||
<td>number</td>
|
||||
<td>是</td>
|
||||
<td>起始偏移量(字节)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>data</td>
|
||||
<td>array</td>
|
||||
<td>是</td>
|
||||
<td>要写入的数据(字节数组)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4>请求示例</h4>
|
||||
<div class="example">
|
||||
[
|
||||
{
|
||||
"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]
|
||||
}
|
||||
]
|
||||
</div>
|
||||
|
||||
<h4>响应示例</h4>
|
||||
<div class="example">
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Parsed Data</h3>
|
||||
<div>
|
||||
<span class="method method-get">GET</span>
|
||||
<code>/api/data/<plc_name>/<area_name></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>
|
||||
</table>
|
||||
|
||||
<h4>响应示例(配置了解析结构)</h4>
|
||||
<div class="example">
|
||||
{
|
||||
"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"
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>响应示例(未配置解析结构)</h4>
|
||||
<div class="example">
|
||||
{
|
||||
"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"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 id="config-api">Configuration API</h2>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Get Configuration</h3>
|
||||
<div>
|
||||
<span class="method method-get">GET</span>
|
||||
<code>/api/config</code>
|
||||
</div>
|
||||
<p>获取当前配置。</p>
|
||||
|
||||
<h4>认证要求</h4>
|
||||
<p>需要Basic Auth认证</p>
|
||||
|
||||
<h4>响应示例</h4>
|
||||
<div class="example">
|
||||
{
|
||||
"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}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Validate Configuration</h3>
|
||||
<div>
|
||||
<span class="method method-post">POST</span>
|
||||
<code>/api/config/validate</code>
|
||||
</div>
|
||||
<p>验证配置是否有效。</p>
|
||||
|
||||
<h4>认证要求</h4>
|
||||
<p>需要Basic Auth认证</p>
|
||||
|
||||
<h4>请求体</h4>
|
||||
<p>要验证的配置JSON</p>
|
||||
|
||||
<h4>响应示例(有效)</h4>
|
||||
<div class="example">
|
||||
{
|
||||
"valid": true
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>响应示例(无效)</h4>
|
||||
<div class="example">
|
||||
{
|
||||
"valid": false,
|
||||
"message": "Invalid configuration: 'ip' is a required property"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Save Configuration</h3>
|
||||
<div>
|
||||
<span class="method method-post">POST</span>
|
||||
<code>/api/config</code>
|
||||
</div>
|
||||
<p>保存配置。</p>
|
||||
|
||||
<h4>查询参数</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>参数</th>
|
||||
<th>描述</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>reload</td>
|
||||
<td>是否立即重载配置(true/false)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4>认证要求</h4>
|
||||
<p>需要Basic Auth认证</p>
|
||||
|
||||
<h4>请求体</h4>
|
||||
<p>要保存的配置JSON</p>
|
||||
|
||||
<h4>响应示例</h4>
|
||||
<div class="example">
|
||||
{
|
||||
"success": true,
|
||||
"message": "Configuration saved and reload requested"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer" style="margin-top: 40px; padding-top: 10px; border-top: 1px solid #ddd; color: #777;">
|
||||
<p>PLC Gateway v1.0 | <a href="/">Back to Status Page</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return render_template_string(html)
|
||||
|
||||
# ===========================
|
||||
# 数据访问API
|
||||
# ===========================
|
||||
@ -495,16 +1114,27 @@ class APIServer:
|
||||
@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):
|
||||
"""从指定区域读取数据"""
|
||||
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/<plc_name>/<area_name>", 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/<plc_name>/<area_name>", methods=["GET"], endpoint="get_parsed_data")
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
from struct import unpack
|
||||
import time
|
||||
|
||||
def parse_data(data, structure):
|
||||
"""解析结构化数据"""
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user