Files
gateway_plc/gateway/api_server.py
2025-08-13 18:13:09 +08:00

1489 lines
66 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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数据访问和配置管理功能"""
def __init__(self, cache_manager, config_path="config/config.json"):
"""
初始化API服务器
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")
# 在初始化方法中调用 setup_routes
self.setup_routes()
def check_auth(self, username, password):
"""验证用户名和密码"""
return username == self.username and password == self.password
def authenticate(self):
"""发送401响应要求认证"""
return Response(
"Unauthorized",
401,
{"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)
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")
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路由"""
# ===========================
# 主页面 - 状态摘要
# ===========================
@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>
"""
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"
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>
"""
for area_name, area in areas.items():
status_class = ""
status_text = area["status"]
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"
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>
"""
html += "</table>"
# 添加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>
"""
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],
"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()),
"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)
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(
html,
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'
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({
"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(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
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(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/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(
target=self.app.run,
kwargs={
"host": "0.0.0.0",
"port": 5000,
"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")