解决了websock频繁连接的问题
This commit is contained in:
@@ -143,7 +143,7 @@ bool PowerManger::Iterate()
|
|||||||
if (m_ticker++ % 5 == 0)
|
if (m_ticker++ % 5 == 0)
|
||||||
{
|
{
|
||||||
m_CcuColCmd.heartbeat++;
|
m_CcuColCmd.heartbeat++;
|
||||||
m_lowerCommManager->processPeriodicTasks();
|
// m_lowerCommManager->processPeriodicTasks();
|
||||||
m_upperCommManager->processPeriodicTasks();
|
m_upperCommManager->processPeriodicTasks();
|
||||||
}
|
}
|
||||||
return(true);
|
return(true);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "mongoose.h"
|
#include "mongoose.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include "json/json.h"
|
#include "json/json.h"
|
||||||
|
#include <sys/resource.h>
|
||||||
|
|
||||||
using json = Json::Value;
|
using json = Json::Value;
|
||||||
|
|
||||||
@@ -11,26 +12,53 @@ using json = Json::Value;
|
|||||||
void HttpServer::handleWebSocketEvent(struct mg_connection* c, int ev, void* ev_data, void* fn_data) {
|
void HttpServer::handleWebSocketEvent(struct mg_connection* c, int ev, void* ev_data, void* fn_data) {
|
||||||
HttpServer* server = static_cast<HttpServer*>(fn_data);
|
HttpServer* server = static_cast<HttpServer*>(fn_data);
|
||||||
|
|
||||||
if (ev == MG_EV_OPEN) {
|
if (ev == MG_EV_WS_OPEN) {
|
||||||
// 新连接建立
|
// WebSocket连接成功建立
|
||||||
std::lock_guard<std::mutex> lock(server->ws_connections_mutex_);
|
std::lock_guard<std::mutex> lock(server->ws_connections_mutex_);
|
||||||
server->ws_connections_.push_back(c);
|
server->ws_connections_.push_back(c);
|
||||||
LOG_F(INFO, "WebSocket connection opened: id=%lu fd=%d", c->id, c->fd);
|
LOG_F(INFO, "WebSocket connection established: id=%lu fd=%d", c->id, c->fd);
|
||||||
} else if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) {
|
} else if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) {
|
||||||
// 连接关闭或错误
|
// 连接关闭或错误
|
||||||
std::lock_guard<std::mutex> lock(server->ws_connections_mutex_);
|
std::lock_guard<std::mutex> lock(server->ws_connections_mutex_);
|
||||||
auto it = std::find(server->ws_connections_.begin(), server->ws_connections_.end(), c);
|
auto it = std::find(server->ws_connections_.begin(), server->ws_connections_.end(), c);
|
||||||
if (it != server->ws_connections_.end()) {
|
if (it != server->ws_connections_.end()) {
|
||||||
LOG_F(ERROR, "WebSocket %s: id=%lu fd=%d",
|
if (ev == MG_EV_ERROR) {
|
||||||
ev == MG_EV_ERROR ? "error" : "closed", c->id, c->fd);
|
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||||
|
if (errno != 0) {
|
||||||
|
LOG_F(ERROR, "WebSocket error: id=%lu fd=%d, errno=%d",
|
||||||
|
c->id, c->fd, errno);
|
||||||
|
// 打印socket错误信息
|
||||||
|
char buf[256];
|
||||||
|
strerror_r(errno, buf, sizeof(buf));
|
||||||
|
LOG_F(ERROR, "Socket error: %s", buf);
|
||||||
|
} else {
|
||||||
|
LOG_F(ERROR, "WebSocket connection closed abnormally: id=%lu fd=%d",
|
||||||
|
c->id, c->fd);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_F(INFO, "WebSocket closed: id=%lu fd=%d", c->id, c->fd);
|
||||||
|
}
|
||||||
server->ws_connections_.erase(it);
|
server->ws_connections_.erase(it);
|
||||||
|
|
||||||
|
// 检查系统资源限制
|
||||||
|
static int fd_limit_checked = 0;
|
||||||
|
if (!fd_limit_checked) {
|
||||||
|
fd_limit_checked = 1;
|
||||||
|
struct rlimit lim;
|
||||||
|
if (getrlimit(RLIMIT_NOFILE, &lim) == 0) {
|
||||||
|
LOG_F(INFO, "File descriptor limits: soft=%lu hard=%lu",
|
||||||
|
lim.rlim_cur, lim.rlim_max);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (ev == MG_EV_HTTP_MSG) {
|
} else if (ev == MG_EV_HTTP_MSG) {
|
||||||
// HTTP请求处理
|
// HTTP请求处理
|
||||||
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||||
if (mg_match(hm->uri, mg_str("/ws"), NULL)) {
|
if (mg_match(hm->uri, mg_str("/ws"), NULL)) {
|
||||||
// 升级到WebSocket连接
|
// 升级到WebSocket连接
|
||||||
|
LOG_F(INFO, "Upgrading to WebSocket: id=%lu fd=%d", c->id, c->fd);
|
||||||
mg_ws_upgrade(c, hm, nullptr);
|
mg_ws_upgrade(c, hm, nullptr);
|
||||||
|
LOG_F(INFO, "WebSocket upgrade completed: id=%lu fd=%d", c->id, c->fd);
|
||||||
} else {
|
} else {
|
||||||
// 返回主页HTML
|
// 返回主页HTML
|
||||||
mg_http_reply(c, 200, "Content-Type: text/html; charset=utf-8\r\n", INDEX_HTML.c_str());
|
mg_http_reply(c, 200, "Content-Type: text/html; charset=utf-8\r\n", INDEX_HTML.c_str());
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 输出文件名
|
|
||||||
OUTPUT_FILE="web_resources.h"
|
|
||||||
|
|
||||||
# 创建输出文件并添加头文件保护
|
|
||||||
echo "// Auto-generated web resources header" > "$OUTPUT_FILE"
|
|
||||||
echo "#ifndef WEB_RESOURCES_H" >> "$OUTPUT_FILE"
|
|
||||||
echo "#define WEB_RESOURCES_H" >> "$OUTPUT_FILE"
|
|
||||||
echo "" >> "$OUTPUT_FILE"
|
|
||||||
|
|
||||||
# 遍历当前目录下的文件
|
|
||||||
for file in *.html *.js *.css; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
# 获取文件名和扩展名
|
|
||||||
filename=$(basename "$file")
|
|
||||||
extension="${filename##*.}"
|
|
||||||
varname="${filename%.*}_${extension}"
|
|
||||||
|
|
||||||
# 使用xxd生成内容并追加到输出文件
|
|
||||||
echo "// Content from $filename" >> "$OUTPUT_FILE"
|
|
||||||
xxd -i "$file" | sed "s/unsigned char .*\[\]/unsigned char ${varname}_content\[\]/" >> "$OUTPUT_FILE"
|
|
||||||
xxd -i "$file" | grep -E "unsigned int .*_len" | sed "s/unsigned int .*_len/unsigned int ${varname}_len/" >> "$OUTPUT_FILE"
|
|
||||||
echo "" >> "$OUTPUT_FILE"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# 添加文件结尾
|
|
||||||
echo "#endif // WEB_RESOURCES_H" >> "$OUTPUT_FILE"
|
|
||||||
|
|
||||||
echo "Web resources header generation complete. Output: $OUTPUT_FILE"
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>H100能源系统仪表盘</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<header class="dashboard-header">
|
|
||||||
<h1><i class="fas fa-bolt"></i> 能源管理系统实时监控</h1>
|
|
||||||
<div class="system-status" id="system-status">
|
|
||||||
<span class="status-indicator"></span>
|
|
||||||
<span>系统运行中</span>
|
|
||||||
<span id="last-update">最后更新: 正在加载...</span>
|
|
||||||
<span class="status-separator">|</span>
|
|
||||||
<span id="device-status">设备: 离线</span>
|
|
||||||
<span class="status-separator">|</span>
|
|
||||||
<span id="comm-status">通信: 断开</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="dashboard-content">
|
|
||||||
<div class="status-cards">
|
|
||||||
<div class="status-card" id="fuel-cell-card">
|
|
||||||
<h2><i class="fas fa-fire"></i> 燃料电池</h2>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="status-item">
|
|
||||||
<span>状态:</span>
|
|
||||||
<span id="fc-status">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span>输出功率:</span>
|
|
||||||
<span id="output-power-limit">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status-card" id="battery-card">
|
|
||||||
<h2><i class="fas fa-battery-three-quarters"></i> 锂电池状态</h2>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="status-item">
|
|
||||||
<span>动力电池:</span>
|
|
||||||
<span id="dyn-soc">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span>仪表电池:</span>
|
|
||||||
<span id="ins-soc">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status-card" id="alarm-card">
|
|
||||||
<h2><i class="fas fa-exclamation-triangle"></i> 报警状态</h2>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="status-item">
|
|
||||||
<span>燃料电池:</span>
|
|
||||||
<span id="fc-fault-level">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span>动力电池:</span>
|
|
||||||
<span id="dyn-alarm-flag1">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span>仪表电池:</span>
|
|
||||||
<span id="ins-alarm-flag1">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span>配电系统:</span>
|
|
||||||
<span id="dis-alarm-flag1">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="data-sections">
|
|
||||||
<button class="collapsible"><i class="fas fa-fire"></i> 燃料电池系统</button>
|
|
||||||
<div class="content">
|
|
||||||
<div class="system-data-grid">
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">工作模式:</span>
|
|
||||||
<span class="data-value" id="fc-mode">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">运行状态:</span>
|
|
||||||
<span class="data-value" id="fc-status">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">输出功率限制:</span>
|
|
||||||
<span class="data-value" id="output-power-limit">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">故障等级:</span>
|
|
||||||
<span class="data-value" id="fc-fault-level">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="collapsible"><i class="fas fa-battery-full"></i> 动力电池系统</button>
|
|
||||||
<div class="content">
|
|
||||||
<div class="system-data-grid">
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">SOC:</span>
|
|
||||||
<span class="data-value" id="dyn-soc">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">母线电压:</span>
|
|
||||||
<span class="data-value" id="dyn-voltage-link">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">正极继电器:</span>
|
|
||||||
<span class="data-value" id="dyn-pos-relay">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">负极继电器:</span>
|
|
||||||
<span class="data-value" id="dyn-neg-relay">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">报警标志1:</span>
|
|
||||||
<span class="data-value" id="dyn-alarm-flag1">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="collapsible"><i class="fas fa-battery-quarter"></i> 仪表电池系统</button>
|
|
||||||
<div class="content">
|
|
||||||
<div class="system-data-grid">
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">SOC:</span>
|
|
||||||
<span class="data-value" id="ins-soc">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">母线电压:</span>
|
|
||||||
<span class="data-value" id="ins-voltage-link">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">正极继电器:</span>
|
|
||||||
<span class="data-value" id="ins-pos-relay">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">负极继电器:</span>
|
|
||||||
<span class="data-value" id="ins-neg-relay">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">报警标志1:</span>
|
|
||||||
<span class="data-value" id="ins-alarm-flag1">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="collapsible"><i class="fas fa-plug"></i> 配电系统</button>
|
|
||||||
<div class="content">
|
|
||||||
<div class="system-data-grid">
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">高压配电状态:</span>
|
|
||||||
<span class="data-value" id="dis-sys-state">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">A相电压:</span>
|
|
||||||
<span class="data-value" id="dis-high-a-vol">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-item">
|
|
||||||
<span class="data-label">B相电压:</span>
|
|
||||||
<span class="data-value" id="dis-high-b-vol">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="collapsible"><i class="fas fa-chart-line"></i> 电流电压曲线</button>
|
|
||||||
<div class="content">
|
|
||||||
<div class="chart-row">
|
|
||||||
<div class="chart-section">
|
|
||||||
<h3>燃料电池电流电压</h3>
|
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="fcVoltageCurrentChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="chart-section">
|
|
||||||
<h3>电池系统电流电压</h3>
|
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="batteryVoltageCurrentChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
<script src="script.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
// 系统状态常量
|
|
||||||
const STATUS = {
|
|
||||||
NORMAL: 'normal',
|
|
||||||
WARNING: 'warning',
|
|
||||||
DANGER: 'danger'
|
|
||||||
};
|
|
||||||
|
|
||||||
// WebSocket连接
|
|
||||||
let ws = null;
|
|
||||||
let reconnectAttempts = 0;
|
|
||||||
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
||||||
const RECONNECT_DELAY = 5000; // 5秒
|
|
||||||
|
|
||||||
// 初始化WebSocket连接
|
|
||||||
function initWebSocket() {
|
|
||||||
ws = new WebSocket(`ws://${window.location.host}/ws`);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
console.log('WebSocket连接已建立');
|
|
||||||
reconnectAttempts = 0;
|
|
||||||
updateSystemStatus(STATUS.NORMAL, '系统连接正常');
|
|
||||||
updateCommStatus(true);
|
|
||||||
updateDeviceStatus(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
updateSystemData(data);
|
|
||||||
updateLastUpdateTime();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解析数据错误:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log('WebSocket连接已关闭');
|
|
||||||
updateCommStatus(false);
|
|
||||||
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
||||||
setTimeout(initWebSocket, RECONNECT_DELAY);
|
|
||||||
reconnectAttempts++;
|
|
||||||
updateSystemStatus(STATUS.WARNING, `连接断开,尝试重新连接 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
||||||
} else {
|
|
||||||
updateSystemStatus(STATUS.DANGER, '连接失败,请刷新页面');
|
|
||||||
updateDeviceStatus(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('WebSocket错误:', error);
|
|
||||||
updateSystemStatus(STATUS.DANGER, '连接错误');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新设备状态
|
|
||||||
function updateDeviceStatus(isOnline) {
|
|
||||||
const element = document.getElementById('device-status');
|
|
||||||
if (element) {
|
|
||||||
element.textContent = `设备: ${isOnline ? '在线' : '离线'}`;
|
|
||||||
element.className = isOnline ? 'status-normal' : 'status-danger';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新通信状态
|
|
||||||
function updateCommStatus(isConnected) {
|
|
||||||
const element = document.getElementById('comm-status');
|
|
||||||
if (element) {
|
|
||||||
element.textContent = `通信: ${isConnected ? '正常' : '断开'}`;
|
|
||||||
element.className = isConnected ? 'status-normal' : 'status-danger';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新系统状态显示
|
|
||||||
function updateSystemStatus(status, message) {
|
|
||||||
const statusElement = document.getElementById('system-status');
|
|
||||||
const indicator = statusElement.querySelector('.status-indicator');
|
|
||||||
|
|
||||||
statusElement.querySelector('span:last-child').textContent = message;
|
|
||||||
|
|
||||||
// 移除所有状态类
|
|
||||||
indicator.classList.remove('status-normal', 'status-warning', 'status-danger');
|
|
||||||
|
|
||||||
// 添加新状态类
|
|
||||||
switch(status) {
|
|
||||||
case STATUS.NORMAL:
|
|
||||||
indicator.classList.add('status-normal');
|
|
||||||
break;
|
|
||||||
case STATUS.WARNING:
|
|
||||||
indicator.classList.add('status-warning');
|
|
||||||
break;
|
|
||||||
case STATUS.DANGER:
|
|
||||||
indicator.classList.add('status-danger');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最后更新时间
|
|
||||||
function updateLastUpdateTime() {
|
|
||||||
const now = new Date();
|
|
||||||
document.getElementById('last-update').textContent = `最后更新: ${now.toLocaleTimeString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新系统数据
|
|
||||||
function updateSystemData(data) {
|
|
||||||
// 燃料电池数据
|
|
||||||
updateDataElement('fc-mode', data.fc_mode, getFCModeText);
|
|
||||||
updateDataElement('fc-status', data.fc_status, getFCStatusText);
|
|
||||||
updateDataElement('output-power-limit', data.output_power_limit, value => `${value} W`);
|
|
||||||
updateDataElement('fc-fault-level', data.fc_fault_level);
|
|
||||||
|
|
||||||
// 电池数据
|
|
||||||
updateDataElement('dyn-soc', data.dyn_soc, value => `${value.toFixed(1)}%`);
|
|
||||||
updateDataElement('ins-soc', data.ins_soc, value => `${value.toFixed(1)}%`);
|
|
||||||
updateDataElement('dyn-voltage-link', data.dyn_voltage_link, value => `${value.toFixed(2)} V`);
|
|
||||||
updateDataElement('ins-voltage-link', data.ins_voltage_link, value => `${value.toFixed(2)} V`);
|
|
||||||
|
|
||||||
// 更新图表数据
|
|
||||||
updateChartData(
|
|
||||||
data.fc_voltage,
|
|
||||||
data.fc_current,
|
|
||||||
data.dyn_voltage_link,
|
|
||||||
data.dyn_current
|
|
||||||
);
|
|
||||||
|
|
||||||
// 报警数据
|
|
||||||
updateDataElement('dyn-alarm-flag1', data.dyn_alarm_flag1, getAlarmStatusText);
|
|
||||||
updateDataElement('ins-alarm-flag1', data.ins_alarm_flag1, getAlarmStatusText);
|
|
||||||
updateDataElement('dis-alarm-flag1', data.dis_alarm_flag1, getAlarmStatusText);
|
|
||||||
|
|
||||||
// 继电器状态
|
|
||||||
updateRelayStatus('ins-pos-relay', data.ins_realy_state?.pos_relay);
|
|
||||||
updateRelayStatus('ins-neg-relay', data.ins_realy_state?.neg_relay);
|
|
||||||
updateRelayStatus('dyn-pos-relay', data.dyn_realy_state?.pos_relay);
|
|
||||||
updateRelayStatus('dyn-neg-relay', data.dyn_realy_state?.neg_relay);
|
|
||||||
|
|
||||||
// 其他数据更新...
|
|
||||||
// 可以根据需要添加更多数据更新逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通用数据更新函数
|
|
||||||
function updateDataElement(elementId, value, formatter = null) {
|
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
if (element && value !== undefined) {
|
|
||||||
element.textContent = formatter ? formatter(value) : value;
|
|
||||||
element.classList.add('updated');
|
|
||||||
setTimeout(() => element.classList.remove('updated'), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新继电器状态
|
|
||||||
function updateRelayStatus(elementId, status) {
|
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
if (element && status !== undefined) {
|
|
||||||
element.textContent = getRelayStatusText(status);
|
|
||||||
element.className = '';
|
|
||||||
|
|
||||||
if (status === 1) {
|
|
||||||
element.classList.add('status-normal');
|
|
||||||
} else {
|
|
||||||
element.classList.add('status-danger');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FC模式文本转换
|
|
||||||
function getFCModeText(mode) {
|
|
||||||
const modes = {
|
|
||||||
0: '无效',
|
|
||||||
1: '调试',
|
|
||||||
2: '自动',
|
|
||||||
3: '补给'
|
|
||||||
};
|
|
||||||
return modes[mode] || '未知模式';
|
|
||||||
}
|
|
||||||
|
|
||||||
// FC状态文本转换
|
|
||||||
function getFCStatusText(status) {
|
|
||||||
const statuses = {
|
|
||||||
0: '无效',
|
|
||||||
1: '初始化',
|
|
||||||
2: '待机',
|
|
||||||
3: '自检异常',
|
|
||||||
4: '就绪',
|
|
||||||
5: '启动',
|
|
||||||
6: '运行',
|
|
||||||
7: '停机',
|
|
||||||
8: '故障',
|
|
||||||
9: '补给',
|
|
||||||
10: '制氢预热',
|
|
||||||
11: '制氢预热完成',
|
|
||||||
12: '停机维护',
|
|
||||||
13: '维护完成'
|
|
||||||
};
|
|
||||||
return statuses[status] || '未知状态';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 继电器状态文本转换
|
|
||||||
function getRelayStatusText(status) {
|
|
||||||
const statuses = {
|
|
||||||
0: '无效',
|
|
||||||
1: '闭合',
|
|
||||||
2: '断开'
|
|
||||||
};
|
|
||||||
return statuses[status] || '未知状态';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 折叠面板功能
|
|
||||||
function setupCollapsibles() {
|
|
||||||
const coll = document.getElementsByClassName("collapsible");
|
|
||||||
for (let i = 0; i < coll.length; i++) {
|
|
||||||
coll[i].addEventListener("click", function() {
|
|
||||||
this.classList.toggle("active");
|
|
||||||
const content = this.nextElementSibling;
|
|
||||||
if (content.style.maxHeight) {
|
|
||||||
content.style.maxHeight = null;
|
|
||||||
} else {
|
|
||||||
content.style.maxHeight = content.scrollHeight + "px";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图表初始化
|
|
||||||
let fcChart = null;
|
|
||||||
let batteryChart = null;
|
|
||||||
const MAX_DATA_POINTS = 60; // 保留60个数据点(约1分钟数据)
|
|
||||||
|
|
||||||
function initCharts() {
|
|
||||||
// 燃料电池图表
|
|
||||||
const fcCtx = document.getElementById('fcVoltageCurrentChart').getContext('2d');
|
|
||||||
fcChart = new Chart(fcCtx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: Array(MAX_DATA_POINTS).fill(''),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: '输出电压 (V)',
|
|
||||||
data: [],
|
|
||||||
borderColor: 'rgb(75, 192, 192)',
|
|
||||||
tension: 0.1,
|
|
||||||
yAxisID: 'y'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '输出电流 (A)',
|
|
||||||
data: [],
|
|
||||||
borderColor: 'rgb(255, 99, 132)',
|
|
||||||
tension: 0.1,
|
|
||||||
yAxisID: 'y1'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
type: 'linear',
|
|
||||||
display: true,
|
|
||||||
position: 'left',
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: '电压 (V)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y1: {
|
|
||||||
type: 'linear',
|
|
||||||
display: true,
|
|
||||||
position: 'right',
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: '电流 (A)'
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
drawOnChartArea: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 电池系统图表
|
|
||||||
const batteryCtx = document.getElementById('batteryVoltageCurrentChart').getContext('2d');
|
|
||||||
batteryChart = new Chart(batteryCtx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: Array(MAX_DATA_POINTS).fill(''),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: '母线电压 (V)',
|
|
||||||
data: [],
|
|
||||||
borderColor: 'rgb(54, 162, 235)',
|
|
||||||
tension: 0.1,
|
|
||||||
yAxisID: 'y'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '电池电流 (A)',
|
|
||||||
data: [],
|
|
||||||
borderColor: 'rgb(255, 159, 64)',
|
|
||||||
tension: 0.1,
|
|
||||||
yAxisID: 'y1'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
type: 'linear',
|
|
||||||
display: true,
|
|
||||||
position: 'left',
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: '电压 (V)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y1: {
|
|
||||||
type: 'linear',
|
|
||||||
display: true,
|
|
||||||
position: 'right',
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: '电流 (A)'
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
drawOnChartArea: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新图表数据
|
|
||||||
function updateChartData(fcVoltage, fcCurrent, batteryVoltage, batteryCurrent) {
|
|
||||||
if (fcChart) {
|
|
||||||
fcChart.data.datasets[0].data.push(fcVoltage);
|
|
||||||
fcChart.data.datasets[1].data.push(fcCurrent);
|
|
||||||
|
|
||||||
if (fcChart.data.datasets[0].data.length > MAX_DATA_POINTS) {
|
|
||||||
fcChart.data.datasets[0].data.shift();
|
|
||||||
fcChart.data.datasets[1].data.shift();
|
|
||||||
}
|
|
||||||
fcChart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (batteryChart) {
|
|
||||||
batteryChart.data.datasets[0].data.push(batteryVoltage);
|
|
||||||
batteryChart.data.datasets[1].data.push(batteryCurrent);
|
|
||||||
|
|
||||||
if (batteryChart.data.datasets[0].data.length > MAX_DATA_POINTS) {
|
|
||||||
batteryChart.data.datasets[0].data.shift();
|
|
||||||
batteryChart.data.datasets[1].data.shift();
|
|
||||||
}
|
|
||||||
batteryChart.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
initWebSocket();
|
|
||||||
setupCollapsibles();
|
|
||||||
initCharts();
|
|
||||||
updateSystemStatus(STATUS.NORMAL, '系统初始化完成');
|
|
||||||
});
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
:root {
|
|
||||||
--primary-color: #3498db;
|
|
||||||
--secondary-color: #2ecc71;
|
|
||||||
--danger-color: #e74c3c;
|
|
||||||
--warning-color: #f39c12;
|
|
||||||
--dark-color: #2c3e50;
|
|
||||||
--light-color: #ecf0f1;
|
|
||||||
--gray-color: #95a5a6;
|
|
||||||
--white-color: #ffffff;
|
|
||||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
--transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
color: var(--dark-color);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-container {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-header h1 {
|
|
||||||
color: var(--dark-color);
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-header h1 i {
|
|
||||||
margin-right: 10px;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
|
||||||
100% { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-cards {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-card {
|
|
||||||
background-color: var(--white-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-card h2 {
|
|
||||||
font-size: 18px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: var(--dark-color);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 折叠面板样式 */
|
|
||||||
.collapsible {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 15px;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
text-align: left;
|
|
||||||
outline: none;
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 10px 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible:after {
|
|
||||||
content: '\002B';
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active:after {
|
|
||||||
content: '\2212';
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 0 15px;
|
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.2s ease-out;
|
|
||||||
background-color: var(--white-color);
|
|
||||||
border-radius: 0 0 5px 5px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-data-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
gap: 15px;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-item {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 5px;
|
|
||||||
border-left: 4px solid var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--dark-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-value {
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 图表样式 */
|
|
||||||
.chart-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-section {
|
|
||||||
background: var(--white-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-container {
|
|
||||||
position: relative;
|
|
||||||
height: 350px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.chart-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
|
||||||
gap: 15px;
|
|
||||||
padding: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-section {
|
|
||||||
background: var(--white-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-section h3 {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--dark-color);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-section h3 i {
|
|
||||||
color: var(--primary-color);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-container {
|
|
||||||
position: relative;
|
|
||||||
height: 300px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-cards {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 状态颜色 */
|
|
||||||
.status-normal {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-warning {
|
|
||||||
color: var(--warning-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-danger {
|
|
||||||
color: var(--danger-color);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user