2025-08-13 18:13:09 +08:00
|
|
|
|
from flask import Flask, jsonify, request, render_template_string, Response
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
import json
|
|
|
|
|
|
from functools import wraps
|
|
|
|
|
|
from config_manager import ConfigManager
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
class APIServer:
|
|
|
|
|
|
"""REST API服务器,提供PLC数据访问和配置管理功能"""
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, cache_manager, config_path="./config/config.json"):
|
2025-08-13 18:13:09 +08:00
|
|
|
|
"""
|
|
|
|
|
|
初始化API服务器
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
Args:
|
|
|
|
|
|
cache_manager: 缓存管理器实例
|
|
|
|
|
|
config_path: 配置文件路径
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.cache_manager = cache_manager
|
|
|
|
|
|
self.config_manager = ConfigManager(config_path)
|
|
|
|
|
|
self.app = Flask(__name__)
|
|
|
|
|
|
self.logger = logging.getLogger("APIServer")
|
|
|
|
|
|
self.auth_enabled = True # 可通过配置关闭认证
|
|
|
|
|
|
self.username = "admin"
|
|
|
|
|
|
self.password = "admin123" # 实际应用中应从安全存储获取
|
|
|
|
|
|
self.start_time = time.strftime("%Y-%m-%d %H:%M:%S")
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
# 在初始化方法中调用 setup_routes
|
|
|
|
|
|
self.setup_routes()
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
def check_auth(self, username, password):
|
|
|
|
|
|
"""验证用户名和密码"""
|
|
|
|
|
|
return username == self.username and password == self.password
|
|
|
|
|
|
|
|
|
|
|
|
def authenticate(self):
|
|
|
|
|
|
"""发送401响应要求认证"""
|
|
|
|
|
|
return Response(
|
2025-08-20 11:48:07 +08:00
|
|
|
|
"Unauthorized",
|
|
|
|
|
|
401,
|
2025-08-13 18:13:09 +08:00
|
|
|
|
{"WWW-Authenticate": 'Basic realm="PLC Gateway Configuration"'}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def requires_auth(self, f):
|
|
|
|
|
|
"""装饰器:需要认证的路由,保留函数元数据"""
|
|
|
|
|
|
@wraps(f)
|
|
|
|
|
|
def decorated(*args, **kwargs):
|
|
|
|
|
|
if not self.auth_enabled:
|
|
|
|
|
|
return f(*args, **kwargs)
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
auth = request.authorization
|
|
|
|
|
|
if not auth or not self.check_auth(auth.username, auth.password):
|
|
|
|
|
|
return self.authenticate()
|
|
|
|
|
|
return f(*args, **kwargs)
|
|
|
|
|
|
return decorated
|
|
|
|
|
|
|
|
|
|
|
|
def get_summary(self):
|
|
|
|
|
|
"""获取缓存摘要信息"""
|
|
|
|
|
|
summary = {}
|
|
|
|
|
|
for plc_name, areas in self.cache_manager.cache.items():
|
|
|
|
|
|
summary[plc_name] = {}
|
|
|
|
|
|
for area_name, area in areas.items():
|
|
|
|
|
|
last_update = self.cache_manager.last_update[plc_name][area_name]
|
|
|
|
|
|
plc_status = self.cache_manager.plc_connection_status.get(plc_name, "unknown")
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
summary[plc_name][area_name] = {
|
|
|
|
|
|
"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"]
|
|
|
|
|
|
}
|
|
|
|
|
|
return summary
|
|
|
|
|
|
|
|
|
|
|
|
def setup_routes(self):
|
|
|
|
|
|
"""设置所有API路由"""
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
# ===========================
|
|
|
|
|
|
# 主页面 - 状态摘要
|
|
|
|
|
|
# ===========================
|
|
|
|
|
|
@self.app.route("/", endpoint="index")
|
|
|
|
|
|
def index():
|
|
|
|
|
|
summary = self.get_summary()
|
|
|
|
|
|
html = """
|
|
|
|
|
|
<html>
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<title>PLC Gateway Status</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
|
|
|
|
h1 { color: #2c3e50; }
|
|
|
|
|
|
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
|
|
|
|
|
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
|
|
|
|
th { background-color: #f2f2f2; }
|
|
|
|
|
|
.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>
|
|
|
|
|
|
<h1>PLC Gateway Status</h1>
|
|
|
|
|
|
<p>Gateway running since: {{ start_time }}</p>
|
|
|
|
|
|
"""
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
for plc_name, areas in summary.items():
|
|
|
|
|
|
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"
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
html += f'<h2 class="{plc_class}">PLC: {plc_name} (Status: {plc_status})</h2>'
|
|
|
|
|
|
html += """
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>Area Name</th>
|
|
|
|
|
|
<th>Type</th>
|
|
|
|
|
|
<th>Size (bytes)</th>
|
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
|
<th>PLC Connection</th>
|
|
|
|
|
|
<th>Last Update</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
"""
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
for area_name, area in areas.items():
|
|
|
|
|
|
status_class = ""
|
|
|
|
|
|
status_text = area["status"]
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
if area["status"] == "connected":
|
|
|
|
|
|
status_class = "status-connected"
|
|
|
|
|
|
elif area["status"] == "never_connected":
|
|
|
|
|
|
status_class = "status-never-connected"
|
|
|
|
|
|
status_text = "Never connected"
|
|
|
|
|
|
elif area["status"] == "disconnected":
|
|
|
|
|
|
status_class = "status-disconnected"
|
|
|
|
|
|
status_text = "Disconnected"
|
|
|
|
|
|
else:
|
|
|
|
|
|
status_class = "status-disconnected"
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
html += f"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{area_name}</td>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
"""
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
html += "</table>"
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
# 添加API文档部分
|
|
|
|
|
|
html += """
|
|
|
|
|
|
<div class="api-section">
|
|
|
|
|
|
<h2>API Endpoints</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="api-endpoint">
|
|
|
|
|
|
<strong>Single Read:</strong> GET /api/read/<plc_name>/<area_name>/<offset>/<length><br>
|
|
|
|
|
|
Example: /api/read/PLC1/DB100_Read/10/4
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="api-endpoint">
|
|
|
|
|
|
<strong>Single Write:</strong> POST /api/write/<plc_name>/<area_name>/<offset><br>
|
|
|
|
|
|
Body: Raw binary data<br>
|
|
|
|
|
|
Example: POST /api/write/PLC1/DB100_Write/10 with 4 bytes of data
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="api-endpoint">
|
|
|
|
|
|
<strong>Single Read_Bool:</strong> GET /api/read_bool/<plc_name>/<area_name>/<offset>/<length><br>
|
|
|
|
|
|
Example: /api/read_bool/PLC1/DB100_Read/0/2
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="api-endpoint">
|
|
|
|
|
|
<strong>Single Write_Bool:</strong> POST /api/write_bool/<plc_name>/<area_name>/<offset><br>
|
|
|
|
|
|
Body: Raw binary data<br>
|
|
|
|
|
|
Example: POST /api/write_bool/PLC1/DB100_Write/0
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="api-endpoint">
|
|
|
|
|
|
<strong>Batch Read:</strong> POST /api/batch_read<br>
|
|
|
|
|
|
Body: JSON array of read requests<br>
|
|
|
|
|
|
Example: [{"plc_name":"PLC1", "area_name":"DB100_Read", "offset":0, "length":4}]
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="api-endpoint">
|
|
|
|
|
|
<strong>Batch Write:</strong> POST /api/batch_write<br>
|
|
|
|
|
|
Body: JSON array of write requests<br>
|
|
|
|
|
|
Example: [{"plc_name":"PLC1", "area_name":"DB100_Write", "offset":0, "data":[1,2,3,4]}]
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="api-endpoint">
|
|
|
|
|
|
<strong>Configuration:</strong> GET/POST /api/config<br>
|
|
|
|
|
|
Manage gateway configuration
|
|
|
|
|
|
</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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="footer">
|
|
|
|
|
|
<p>PLC Gateway v1.0 | <a href="/api/status">System Status</a></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
"""
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
html += """
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
"""
|
|
|
|
|
|
return render_template_string(html, start_time=self.start_time)
|
|
|
|
|
|
|
|
|
|
|
|
# ===========================
|
|
|
|
|
|
# 系统状态API
|
|
|
|
|
|
# ===========================
|
|
|
|
|
|
@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],
|
2025-08-20 11:48:07 +08:00
|
|
|
|
"last_connected": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.cache_manager.plc_last_connected[plc_name]))
|
2025-08-13 18:13:09 +08:00
|
|
|
|
if self.cache_manager.plc_last_connected[plc_name] > 0 else "Never"
|
|
|
|
|
|
}
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
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()),
|
|
|
|
|
|
"plc_statuses": plc_statuses
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
# ===========================
|
|
|
|
|
|
# 配置管理相关路由
|
|
|
|
|
|
# ===========================
|
|
|
|
|
|
@self.app.route("/config", endpoint="config_page")
|
|
|
|
|
|
@self.requires_auth
|
|
|
|
|
|
def config_page():
|
|
|
|
|
|
"""配置编辑页面"""
|
|
|
|
|
|
config = self.config_manager.get_config()
|
|
|
|
|
|
config_json = json.dumps(config, indent=2)
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
html = """
|
|
|
|
|
|
<html>
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<title>PLC Gateway Configuration</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
|
|
|
|
h1 { color: #2c3e50; }
|
|
|
|
|
|
.config-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
textarea {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 500px;
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.button-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
button {
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
background-color: #3498db;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
button:hover {
|
|
|
|
|
|
background-color: #2980b9;
|
|
|
|
|
|
}
|
|
|
|
|
|
.status-message {
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.success { background-color: #d4edda; color: #155724; }
|
|
|
|
|
|
.error { background-color: #f8d7da; color: #721c24; }
|
|
|
|
|
|
.info { background-color: #d1ecf1; color: #0c5460; }
|
|
|
|
|
|
.config-help {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
|
border-left: 4px solid #3498db;
|
|
|
|
|
|
}
|
|
|
|
|
|
.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>
|
|
|
|
|
|
|
|
|
|
|
|
<form id="configForm">
|
|
|
|
|
|
<textarea id="configEditor" name="config">{{ config_json }}</textarea>
|
|
|
|
|
|
<div class="button-group">
|
|
|
|
|
|
<button type="button" onclick="validateConfig()">Validate</button>
|
|
|
|
|
|
<button type="button" onclick="saveConfig(false)">Save</button>
|
|
|
|
|
|
<button type="button" onclick="saveConfig(true)">Save & Reload</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="statusMessage" class="status-message"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="config-help">
|
|
|
|
|
|
<h3>Configuration Guide</h3>
|
|
|
|
|
|
<p><strong>PLC Configuration:</strong></p>
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li><code>name</code>: Unique name for the PLC</li>
|
|
|
|
|
|
<li><code>ip</code>: IP address of the PLC</li>
|
|
|
|
|
|
<li><code>rack</code>: Rack number (usually 0)</li>
|
|
|
|
|
|
<li><code>slot</code>: Slot number (usually 1 for S7-1200)</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
|
|
<p><strong>Data Area Configuration:</strong></p>
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li><code>name</code>: Name of the data area</li>
|
|
|
|
|
|
<li><code>type</code>: <code>read</code>, <code>write</code>, or <code>read_write</code></li>
|
|
|
|
|
|
<li><code>db_number</code>: DB number (e.g., 100 for DB100)</li>
|
|
|
|
|
|
<li><code>offset</code>: Starting byte offset</li>
|
|
|
|
|
|
<li><code>size</code>: Size in bytes</li>
|
|
|
|
|
|
<li><code>structure</code> (optional): Define how to parse the data</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
|
|
<p><strong>Example:</strong></p>
|
|
|
|
|
|
<pre>{
|
|
|
|
|
|
"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}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
function showStatus(message, type) {
|
|
|
|
|
|
const statusDiv = document.getElementById('statusMessage');
|
|
|
|
|
|
statusDiv.textContent = message;
|
|
|
|
|
|
statusDiv.className = 'status-message ' + type;
|
|
|
|
|
|
statusDiv.style.display = 'block';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function validateConfig() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const config = JSON.parse(document.getElementById('configEditor').value);
|
|
|
|
|
|
fetch('/api/config/validate', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Authorization': 'Basic ' + btoa('{{ username }}:{{ password }}')
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify(config)
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(response => {
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
return response.json().then(err => { throw new Error(err.message || 'Validation failed'); });
|
|
|
|
|
|
}
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
if (data.valid) {
|
|
|
|
|
|
showStatus('Configuration is valid!', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showStatus('Validation error: ' + data.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
showStatus('Validation error: ' + error.message, 'error');
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showStatus('JSON error: ' + e.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function saveConfig(reload) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const config = JSON.parse(document.getElementById('configEditor').value);
|
|
|
|
|
|
const url = reload ? '/api/config?reload=true' : '/api/config';
|
|
|
|
|
|
|
|
|
|
|
|
fetch(url, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Authorization': 'Basic ' + btoa('{{ username }}:{{ password }}')
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify(config)
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(response => {
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
return response.json().then(err => { throw new Error(err.message || 'Save failed'); });
|
|
|
|
|
|
}
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
const msg = reload ?
|
|
|
|
|
|
'Configuration saved and reloaded successfully!' :
|
|
|
|
|
|
'Configuration saved successfully. Restart to apply changes.';
|
|
|
|
|
|
showStatus(msg, 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showStatus('Save error: ' + data.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
showStatus('Error: ' + error.message, 'error');
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showStatus('JSON error: ' + e.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
"""
|
|
|
|
|
|
return render_template_string(
|
2025-08-20 11:48:07 +08:00
|
|
|
|
html,
|
2025-08-13 18:13:09 +08:00
|
|
|
|
config_json=config_json,
|
|
|
|
|
|
username=self.username,
|
|
|
|
|
|
password=self.password
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 配置验证API
|
|
|
|
|
|
@self.app.route("/api/config/validate", methods=["POST"], endpoint="validate_config")
|
|
|
|
|
|
@self.requires_auth
|
|
|
|
|
|
def validate_config():
|
|
|
|
|
|
"""验证配置是否有效"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
config = request.json
|
|
|
|
|
|
is_valid, error = self.config_manager.validate_config(config)
|
|
|
|
|
|
if is_valid:
|
|
|
|
|
|
return jsonify({"valid": True})
|
|
|
|
|
|
else:
|
|
|
|
|
|
return jsonify({"valid": False, "message": error}), 400
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return jsonify({"valid": False, "message": str(e)}), 400
|
|
|
|
|
|
|
|
|
|
|
|
# 配置获取API
|
|
|
|
|
|
@self.app.route("/api/config", methods=["GET"], endpoint="get_config")
|
|
|
|
|
|
@self.requires_auth
|
|
|
|
|
|
def get_config():
|
|
|
|
|
|
"""获取当前配置"""
|
|
|
|
|
|
return jsonify(self.config_manager.get_config())
|
|
|
|
|
|
|
|
|
|
|
|
# 配置保存API
|
|
|
|
|
|
@self.app.route("/api/config", methods=["POST"], endpoint="save_config")
|
|
|
|
|
|
@self.requires_auth
|
|
|
|
|
|
def save_config():
|
|
|
|
|
|
"""保存配置"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
config = request.json
|
|
|
|
|
|
reload = request.args.get('reload', 'false').lower() == 'true'
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
success, message = self.config_manager.save_config(config)
|
|
|
|
|
|
if success:
|
|
|
|
|
|
if reload:
|
|
|
|
|
|
# 通知主应用程序重载配置
|
|
|
|
|
|
if hasattr(self.cache_manager, 'app') and self.cache_manager.app:
|
|
|
|
|
|
self.cache_manager.app.request_reload()
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"message": "Configuration saved and reload requested"
|
|
|
|
|
|
})
|
|
|
|
|
|
else:
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"message": "Configuration saved successfully (restart to apply changes)"
|
|
|
|
|
|
})
|
|
|
|
|
|
else:
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"success": False,
|
|
|
|
|
|
"message": message
|
|
|
|
|
|
}), 400
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"success": False,
|
|
|
|
|
|
"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>Single Read Bool</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>
|
|
|
|
|
|
<span class="method method-post">POST</span>
|
|
|
|
|
|
<code>/api/write_bool/<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>{0:True}</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h4>响应示例</h4>
|
|
|
|
|
|
<div class="example">
|
|
|
|
|
|
{
|
|
|
|
|
|
"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"
|
|
|
|
|
|
}
|
|
|
|
|
|
</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
|
|
|
|
|
|
# ===========================
|
|
|
|
|
|
# 单个读取接口
|
|
|
|
|
|
@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, plc_status, update_time = self.cache_manager.read_area(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": 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))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
# 单个读取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")
|
|
|
|
|
|
def single_write(plc_name, area_name, offset):
|
|
|
|
|
|
"""向指定区域写入数据"""
|
|
|
|
|
|
data = request.data
|
|
|
|
|
|
if not data:
|
|
|
|
|
|
# 如果没有提供数据,返回错误
|
|
|
|
|
|
return jsonify({
|
2025-08-20 11:48:07 +08:00
|
|
|
|
"status": "error",
|
2025-08-13 18:13:09 +08:00
|
|
|
|
"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
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
success, error, plc_status, update_time = self.cache_manager.write_area(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": 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))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
# 单个写入BOOL类型接口
|
|
|
|
|
|
@self.app.route("/api/write_bool/<plc_name>/<area_name>/<int:offset>", 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():
|
|
|
|
|
|
"""批量读取多个区域的数据"""
|
|
|
|
|
|
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.get_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 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)
|
|
|
|
|
|
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
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
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
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
self.logger.info(f"Received batch write request: {json.dumps(requests)}")
|
2025-08-20 11:48:07 +08:00
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
results = self.cache_manager.batch_write(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
|
|
|
|
|
|
|
2025-08-20 11:48:07 +08:00
|
|
|
|
# 通用读取接口
|
|
|
|
|
|
@self.app.route("/api/read_generic/<plc_name>/<area_name>/<int:offset>/<data_type>", methods=["GET"],
|
|
|
|
|
|
endpoint="read_generic")
|
|
|
|
|
|
def read_generic(plc_name, area_name, offset, data_type):
|
|
|
|
|
|
"""通用读取接口"""
|
|
|
|
|
|
# 检查请求参数
|
|
|
|
|
|
count = request.args.get('count', 1, type=int)
|
|
|
|
|
|
if count < 1:
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"message": "Count must be at least 1",
|
|
|
|
|
|
"plc_name": plc_name,
|
|
|
|
|
|
"area_name": area_name
|
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
|
|
# 执行读取
|
|
|
|
|
|
result, error, plc_status, update_time = self.cache_manager.read_generic(
|
|
|
|
|
|
plc_name,
|
|
|
|
|
|
area_name,
|
|
|
|
|
|
offset,
|
|
|
|
|
|
data_type,
|
|
|
|
|
|
count
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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": count,
|
|
|
|
|
|
"data": result,
|
|
|
|
|
|
"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_generic/<plc_name>/<area_name>/<int:offset>/<data_type>", methods=["POST"],
|
|
|
|
|
|
endpoint="write_generic")
|
|
|
|
|
|
def write_generic(plc_name, area_name, offset, data_type):
|
|
|
|
|
|
"""通用写入接口"""
|
|
|
|
|
|
# 检查请求数据
|
|
|
|
|
|
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.get_json()
|
|
|
|
|
|
if "value" not in json_data and "values" not in json_data:
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"message": "Missing 'value' or 'values' field",
|
|
|
|
|
|
"plc_name": plc_name,
|
|
|
|
|
|
"area_name": area_name
|
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
|
|
# 确定要写入的值
|
|
|
|
|
|
value = json_data.get("value", json_data.get("values"))
|
|
|
|
|
|
|
|
|
|
|
|
# 执行写入
|
|
|
|
|
|
success, error, plc_status, update_time = self.cache_manager.write_generic(
|
|
|
|
|
|
plc_name,
|
|
|
|
|
|
area_name,
|
|
|
|
|
|
offset,
|
|
|
|
|
|
data_type,
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
# 确定写入数量
|
|
|
|
|
|
count = 1
|
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
|
count = len(value)
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"status": "success",
|
|
|
|
|
|
"plc_name": plc_name,
|
|
|
|
|
|
"area_name": area_name,
|
|
|
|
|
|
"offset": offset,
|
|
|
|
|
|
"data_type": data_type,
|
|
|
|
|
|
"count": count,
|
|
|
|
|
|
"plc_connection_status": plc_status,
|
|
|
|
|
|
"last_update": update_time,
|
|
|
|
|
|
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-08-13 18:13:09 +08:00
|
|
|
|
@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")
|
|
|
|
|
|
def 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")
|
|
|
|
|
|
def get_parsed_data(plc_name, area_name):
|
|
|
|
|
|
"""获取解析后的数据"""
|
|
|
|
|
|
return jsonify(self.cache_manager.get_parsed_data(plc_name, area_name))
|
|
|
|
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
|
|
"""启动API服务器"""
|
|
|
|
|
|
self.server_thread = threading.Thread(
|
2025-08-20 11:48:07 +08:00
|
|
|
|
target=self.app.run,
|
2025-08-13 18:13:09 +08:00
|
|
|
|
kwargs={
|
2025-08-20 11:48:07 +08:00
|
|
|
|
"host": "0.0.0.0",
|
|
|
|
|
|
"port": 5000,
|
2025-08-13 18:13:09 +08:00
|
|
|
|
"threaded": True,
|
|
|
|
|
|
"use_reloader": False # 避免在生产环境中使用重载器
|
|
|
|
|
|
},
|
|
|
|
|
|
daemon=True,
|
|
|
|
|
|
name="APIServerThread"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.server_thread.start()
|
|
|
|
|
|
self.logger.info("API server started at http://0.0.0.0:5000")
|