- pmSysvariable/ccuUdpMsg: 域起始符改为0x40 0x40,域标识符重编号0x0001~0x0004, ccuState/ccuColCmd/ccuSetParmCmd 按新协议字段布局与字节序重写(状态248B/操控45B/参数28B) - systemData/httpserver: 新增FC单片电压、甲醇制氢装置、DC/DC、尾气装置、一体化罐、 供氢供氧流量、应急上浮等字段解析与WebSocket JSON推送 - 新增 /fcs 燃料电池监控页(fuelcell.h):实时数据显示 + 操控指令交互按钮, 过滤日志推送消息避免界面闪烁 - SQLite/LowerCommManager/测试:更新建表列、解析打印与单元/集成测试适配新协议
894 lines
35 KiB
C++
894 lines
35 KiB
C++
#ifndef HTTP_SERVER_FUELCELL_H
|
|
#define HTTP_SERVER_FUELCELL_H
|
|
|
|
#include <string>
|
|
|
|
const std::string FUELCELL_HTML = R"(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>燃料电池</title>
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
<style>
|
|
:root {
|
|
--primary-color: #2563eb;
|
|
--primary-hover: #1d4ed8;
|
|
--bg-color: #f8fafc;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
background-color: var(--bg-color);
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.container {
|
|
display: grid;
|
|
grid-template-areas:
|
|
"sidebar main-content";
|
|
grid-template-columns: 220px 1fr;
|
|
grid-template-rows: 1fr;
|
|
min-height: 100vh;
|
|
gap: 0;
|
|
}
|
|
|
|
.header {
|
|
grid-area: unset;
|
|
background: white;
|
|
padding: 0 0.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
left: 220px;
|
|
height: 36px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.sidebar {
|
|
grid-area: sidebar;
|
|
background: white;
|
|
width: 220px;
|
|
height: 100vh;
|
|
position: fixed;
|
|
left: 0;
|
|
top: 0;
|
|
padding: 0.5rem 0.25rem;
|
|
box-shadow: 1px 0 3px rgba(0, 0, 0, 0.1);
|
|
z-index: 20;
|
|
}
|
|
|
|
.sidebar-menu {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.sidebar-button {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.5rem 0.75rem;
|
|
border-radius: 0.375rem;
|
|
color: #4b5563;
|
|
text-decoration: none;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.sidebar-button:hover {
|
|
background: var(--bg-color);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.sidebar-icon {
|
|
min-width: 1.5rem;
|
|
margin-right: 1rem;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.main-content {
|
|
grid-area: main-content;
|
|
padding: 1rem;
|
|
margin-top: 36px;
|
|
}
|
|
|
|
.cards-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.data-card {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
border: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.data-card h3 {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
margin-bottom: 0.75rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 2px solid #e5e7eb;
|
|
}
|
|
|
|
.data-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 6px 4px;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
min-height: 28px;
|
|
}
|
|
|
|
.data-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.data-row span:first-child {
|
|
color: #4b5563;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.data-row span:last-child {
|
|
color: #1a1a1a;
|
|
font-weight: 600;
|
|
font-family: 'SF Mono', 'Consolas', monospace;
|
|
text-align: right;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.badge {
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.badge.on {
|
|
background-color: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.badge.off {
|
|
background-color: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.badge.warn {
|
|
background-color: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.control-panel {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
margin-bottom: 16px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.control-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.control-label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.radio-group {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.radio-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.button {
|
|
padding: 8px 14px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background-color: #e5e7eb;
|
|
color: #1a1a1a;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.button:hover {
|
|
background-color: #d1d5db;
|
|
}
|
|
|
|
.button-secondary {
|
|
background-color: #e5e7eb;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.button-secondary:hover {
|
|
background-color: #d1d5db;
|
|
}
|
|
|
|
.button-active {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
}
|
|
|
|
.button-active:hover {
|
|
background-color: var(--primary-hover);
|
|
}
|
|
|
|
.button-danger {
|
|
background-color: #dc2626;
|
|
color: white;
|
|
}
|
|
|
|
.button-danger:hover {
|
|
background-color: #b91c1c;
|
|
}
|
|
|
|
.select-control {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 4px;
|
|
background-color: white;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.slider-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.slider {
|
|
flex-grow: 1;
|
|
height: 6px;
|
|
-webkit-appearance: none;
|
|
background: #d1d5db;
|
|
border-radius: 3px;
|
|
outline: none;
|
|
}
|
|
|
|
.slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
background: var(--primary-color);
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.slider-value {
|
|
min-width: 60px;
|
|
text-align: right;
|
|
font-family: 'SF Mono', 'Consolas', monospace;
|
|
}
|
|
|
|
.form-submit {
|
|
margin-top: 24px;
|
|
width: 100%;
|
|
padding: 12px;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.status-message {
|
|
margin-top: 16px;
|
|
padding: 12px;
|
|
border-radius: 4px;
|
|
background-color: #f0fdf4;
|
|
color: #166534;
|
|
display: none;
|
|
}
|
|
|
|
.status-message.error {
|
|
background-color: #fef2f2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.two-col {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0 20px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.two-col {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="sidebar">
|
|
<div class="sidebar-menu">
|
|
<a href="/" class="sidebar-button">
|
|
<span class="sidebar-icon">🏠</span>
|
|
<span>系统主页</span>
|
|
</a>
|
|
<a href="/data" class="sidebar-button">
|
|
<span class="sidebar-icon">📊</span>
|
|
<span>系统数据</span>
|
|
</a>
|
|
<a href="/dashboard" class="sidebar-button">
|
|
<span class="sidebar-icon">⚡</span>
|
|
<span>配电操作</span>
|
|
</a>
|
|
<a href="/battery" class="sidebar-button">
|
|
<span class="sidebar-icon">🔋</span>
|
|
<span>电池操作</span>
|
|
</a>
|
|
<a href="/fcs" class="sidebar-button">
|
|
<span class="sidebar-icon">🔥</span>
|
|
<span>燃料电池</span>
|
|
</a>
|
|
<a href="/logs" class="sidebar-button">
|
|
<span class="sidebar-icon">📝</span>
|
|
<span>系统日志</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="header">
|
|
<h1>燃料电池系统监控</h1>
|
|
<span id="commState" class="badge off">通讯中断</span>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<div class="cards-grid">
|
|
<!-- 系统总体 -->
|
|
<div class="data-card">
|
|
<h3>系统总体</h3>
|
|
<div class="data-row"><span>运行模式:</span><span id="fc-mode">--</span></div>
|
|
<div class="data-row"><span>运行状态:</span><span id="fc-status">--</span></div>
|
|
<div class="data-row"><span>系统故障等级:</span><span id="fc-fault-level">--</span></div>
|
|
<div class="data-row"><span>一级故障码:</span><span id="fault-level-1">--</span></div>
|
|
<div class="data-row"><span>二级故障码:</span><span id="fault-level-2">--</span></div>
|
|
<div class="data-row"><span>三级故障码:</span><span id="fault-level-3">--</span></div>
|
|
<div class="data-row"><span>四级故障码:</span><span id="fault-level-4">--</span></div>
|
|
<div class="data-row"><span>累积发电时间:</span><span id="total-gen-time">--</span></div>
|
|
<div class="data-row"><span>输出功率限定:</span><span id="output-power-limit">--</span></div>
|
|
<div class="data-row"><span>系统发电功率:</span><span id="total-gen-power">--</span></div>
|
|
<div class="data-row"><span>储氢剩余容量:</span><span id="hydrogen-capacity">--</span></div>
|
|
<div class="data-row"><span>液氧剩余容量:</span><span id="liquid-oxygen-capacity">--</span></div>
|
|
<div class="data-row"><span>通信心跳:</span><span id="heartbeat">--</span></div>
|
|
<div class="data-row"><span>应急指令:</span><span id="emergency-command">--</span></div>
|
|
</div>
|
|
|
|
<!-- FC单片电压 -->
|
|
<div class="data-card">
|
|
<h3>FC单片电压</h3>
|
|
<div class="data-row"><span>I#FC最低单片电压:</span><span id="fc1-min-cell-v">--</span></div>
|
|
<div class="data-row"><span>I#FC最低单片电压位置:</span><span id="fc1-min-cell-pos">--</span></div>
|
|
<div class="data-row"><span>I#FC平均单片电压:</span><span id="fc1-avg-cell-v">--</span></div>
|
|
<div class="data-row"><span>2#FC最低单片电压:</span><span id="fc2-min-cell-v">--</span></div>
|
|
<div class="data-row"><span>2#FC最低单片电压位置:</span><span id="fc2-min-cell-pos">--</span></div>
|
|
<div class="data-row"><span>2#FC平均单片电压:</span><span id="fc2-avg-cell-v">--</span></div>
|
|
</div>
|
|
|
|
<!-- 甲醇制氢装置 -->
|
|
<div class="data-card">
|
|
<h3>甲醇制氢装置</h3>
|
|
<div class="data-row"><span>钯膜最高温度:</span><span id="palladium-temp">--</span></div>
|
|
<div class="data-row"><span>缓冲罐压力:</span><span id="buffer-tank-pressure">--</span></div>
|
|
<div class="data-row"><span>烟气累计排放量:</span><span id="flue-total-emission">--</span></div>
|
|
<div class="data-row"><span>烟气压力:</span><span id="flue-pressure">--</span></div>
|
|
<div class="data-row"><span>反应器压力:</span><span id="reactor-pressure">--</span></div>
|
|
<div class="data-row"><span>电动阀开度:</span><span id="electric-valve-open">--</span></div>
|
|
</div>
|
|
|
|
<!-- DC/DC -->
|
|
<div class="data-card">
|
|
<h3>DC/DC</h3>
|
|
<div class="data-row"><span>通道1输入电压:</span><span id="dcdc1-in-voltage">--</span></div>
|
|
<div class="data-row"><span>通道1输入电流:</span><span id="dcdc1-in-current">--</span></div>
|
|
<div class="data-row"><span>通道2输入电压:</span><span id="dcdc2-in-voltage">--</span></div>
|
|
<div class="data-row"><span>通道2输入电流:</span><span id="dcdc2-in-current">--</span></div>
|
|
<div class="data-row"><span>输出电压:</span><span id="dcdc-out-voltage">--</span></div>
|
|
<div class="data-row"><span>输出电流:</span><span id="dcdc-out-current">--</span></div>
|
|
<div class="data-row"><span>控制电源电压:</span><span id="dcdc-control-voltage">--</span></div>
|
|
<div class="data-row"><span>辅电输出电压:</span><span id="dcdc-aux-out-voltage">--</span></div>
|
|
</div>
|
|
|
|
<!-- 甲醇/水系统 -->
|
|
<div class="data-card">
|
|
<h3>甲醇/水系统</h3>
|
|
<div class="data-row"><span>甲醇累计使用量:</span><span id="methanol-total-usage">--</span></div>
|
|
<div class="data-row"><span>甲醇溶液进料量:</span><span id="methanol-solution-feed">--</span></div>
|
|
<div class="data-row"><span>氧侧生成水箱液位:</span><span id="oxygen-water-tank-level">--</span></div>
|
|
<div class="data-row"><span>氢侧生成水箱液位:</span><span id="hydrogen-water-tank-level">--</span></div>
|
|
<div class="data-row"><span>配重水箱液位:</span><span id="ballast-water-tank-level">--</span></div>
|
|
</div>
|
|
|
|
<!-- 尾气装置 -->
|
|
<div class="data-card">
|
|
<h3>尾气装置</h3>
|
|
<div class="data-row"><span>进气压力:</span><span id="exhaust-in-pressure">--</span></div>
|
|
<div class="data-row"><span>排气压力:</span><span id="exhaust-out-pressure">--</span></div>
|
|
<div class="data-row"><span>运行频率:</span><span id="exhaust-run-freq">--</span></div>
|
|
<div class="data-row"><span>进气温度:</span><span id="exhaust-in-temp">--</span></div>
|
|
<div class="data-row"><span>排气温度:</span><span id="exhaust-out-temp">--</span></div>
|
|
<div class="data-row"><span>进水压力:</span><span id="exhaust-in-water-pressure">--</span></div>
|
|
<div class="data-row"><span>排水压力:</span><span id="exhaust-out-water-pressure">--</span></div>
|
|
</div>
|
|
|
|
<!-- 一体化罐装置 -->
|
|
<div class="data-card">
|
|
<h3>一体化罐装置</h3>
|
|
<div class="data-row"><span>液氧罐压力:</span><span id="tank-lo2-pressure">--</span></div>
|
|
<div class="data-row"><span>二氧化碳压力:</span><span id="tank-co2-pressure">--</span></div>
|
|
<div class="data-row"><span>液氧罐液位:</span><span id="tank-lo2-level">--</span></div>
|
|
</div>
|
|
|
|
<!-- 供氢/供氧流量 -->
|
|
<div class="data-card">
|
|
<h3>供氢/供氧流量</h3>
|
|
<div class="data-row"><span>合金供氢流量:</span><span id="alloy-h2-flow">--</span></div>
|
|
<div class="data-row"><span>FC供氢流量:</span><span id="fc-h2-flow">--</span></div>
|
|
<div class="data-row"><span>FC供氧流量:</span><span id="fc-o2-flow">--</span></div>
|
|
</div>
|
|
|
|
<!-- 应急上浮 -->
|
|
<div class="data-card">
|
|
<h3>应急上浮</h3>
|
|
<div class="data-row"><span>应急上浮深度:</span><span id="emergency-float-depth">--</span></div>
|
|
<div class="data-row"><span>应急上浮时间:</span><span id="emergency-float-time">--</span></div>
|
|
</div>
|
|
|
|
<!-- 舱室环境 -->
|
|
<div class="data-card">
|
|
<h3>舱室环境</h3>
|
|
<div class="data-row"><span>舱室压力1:</span><span id="cabin-pressure1">--</span></div>
|
|
<div class="data-row"><span>舱室压力2:</span><span id="cabin-pressure2">--</span></div>
|
|
<div class="data-row"><span>舱室温度1:</span><span id="cabin-temp1">--</span></div>
|
|
<div class="data-row"><span>舱室温度2:</span><span id="cabin-temp2">--</span></div>
|
|
<div class="data-row"><span>舱室湿度1:</span><span id="cabin-humidity1">--</span></div>
|
|
<div class="data-row"><span>舱室湿度2:</span><span id="cabin-humidity2">--</span></div>
|
|
<div class="data-row"><span>H2浓度1:</span><span id="h2-concentration1">--</span></div>
|
|
<div class="data-row"><span>H2浓度2:</span><span id="h2-concentration2">--</span></div>
|
|
<div class="data-row"><span>H2浓度3:</span><span id="h2-concentration3">--</span></div>
|
|
<div class="data-row"><span>O2浓度1:</span><span id="o2-concentration1">--</span></div>
|
|
<div class="data-row"><span>O2浓度2:</span><span id="o2-concentration2">--</span></div>
|
|
<div class="data-row"><span>甲醇浓度1:</span><span id="ch3oh-concentration1">--</span></div>
|
|
<div class="data-row"><span>甲醇浓度2:</span><span id="ch3oh-concentration2">--</span></div>
|
|
<div class="data-row"><span>火焰探测器1:</span><span id="flame-detector1">--</span></div>
|
|
<div class="data-row"><span>火焰探测器2:</span><span id="flame-detector2">--</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 指令交互 -->
|
|
<div class="control-panel">
|
|
<div class="panel-title">燃料电池操控指令</div>
|
|
|
|
<form id="fcsForm" method="POST" action="/fcs/control">
|
|
<div class="control-group">
|
|
<label class="control-label">模式设定</label>
|
|
<div class="radio-group">
|
|
<label class="radio-option"><input type="radio" name="fcMode" value="00H" checked><span>无效</span></label>
|
|
<label class="radio-option"><input type="radio" name="fcMode" value="01H"><span>调试</span></label>
|
|
<label class="radio-option"><input type="radio" name="fcMode" value="02H"><span>自动</span></label>
|
|
<label class="radio-option"><input type="radio" name="fcMode" value="03H"><span>补给/排放</span></label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">操控指令</label>
|
|
<select class="select-control" name="fcCommand">
|
|
<option value="00H">无效</option>
|
|
<option value="01H">自检</option>
|
|
<option value="02H">启动</option>
|
|
<option value="03H">停机</option>
|
|
<option value="04H">复位</option>
|
|
<option value="05H">紧急停机</option>
|
|
<option value="06H">补给</option>
|
|
<option value="07H">制氢预热</option>
|
|
<option value="08H">停机维护</option>
|
|
<option value="0AH">开机</option>
|
|
<option value="0BH">关机</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">输出功率指令 (0~250,分辨率0.1kW)</label>
|
|
<div class="slider-container">
|
|
<input type="range" class="slider" name="fcPower" min="0" max="250" value="0" step="1">
|
|
<span class="slider-value">0 (0.0 kW)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">姿态数据(分辨率0.1°)</label>
|
|
<div class="two-col">
|
|
<div class="control-group">
|
|
<label class="control-label">纵倾姿态1</label>
|
|
<input type="number" name="pitch1" min="0" max="255" value="0" class="select-control">
|
|
<label class="control-label">纵倾姿态2</label>
|
|
<input type="number" name="pitch2" min="0" max="255" value="0" class="select-control">
|
|
</div>
|
|
<div class="control-group">
|
|
<label class="control-label">横倾姿态1</label>
|
|
<input type="number" name="roll1" min="0" max="255" value="0" class="select-control">
|
|
<label class="control-label">横倾姿态2</label>
|
|
<input type="number" name="roll2" min="0" max="255" value="0" class="select-control">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">应急允许</label>
|
|
<div class="button-group">
|
|
<button type="button" class="button button-active" data-command="emergencyAllow" data-value="00H">无</button>
|
|
<button type="button" class="button button-secondary" data-command="emergencyAllow" data-value="01H">允许降载</button>
|
|
<button type="button" class="button button-secondary" data-command="emergencyAllow" data-value="02H">允许排气</button>
|
|
<button type="button" class="button button-secondary" data-command="emergencyAllow" data-value="03H">降载+排气</button>
|
|
</div>
|
|
<input type="hidden" name="emergencyAllow" value="00H">
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">潜深深度 (m)</label>
|
|
<div class="slider-container">
|
|
<input type="range" class="slider" name="depth" min="0" max="1000" value="0" step="1">
|
|
<span class="slider-value">0 m</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">补给/排放指令</label>
|
|
<select class="select-control" name="supplyExhaustCmd">
|
|
<option value="00H">无效</option>
|
|
<option value="01H">液氧加注</option>
|
|
<option value="02H">甲醇加注</option>
|
|
<option value="03H">液氧排空</option>
|
|
<option value="04H">甲醇排空</option>
|
|
<option value="05H">CO2排空</option>
|
|
<option value="07H">合金加注</option>
|
|
<option value="08H">氮气加注</option>
|
|
<option value="09H">纯水加注</option>
|
|
<option value="0AH">备用</option>
|
|
<option value="0BH">备用</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">仪表锂电池启停指令</label>
|
|
<div class="button-group">
|
|
<button type="button" class="button button-active" data-command="insBatCmd" data-value="00H">无效</button>
|
|
<button type="button" class="button button-secondary" data-command="insBatCmd" data-value="10H">启动</button>
|
|
<button type="button" class="button button-secondary" data-command="insBatCmd" data-value="20H">关闭</button>
|
|
</div>
|
|
<input type="hidden" name="insBatCmd" value="00H">
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">动力锂电池启停指令</label>
|
|
<div class="button-group">
|
|
<button type="button" class="button button-active" data-command="powerBatCmd" data-value="00H">无效</button>
|
|
<button type="button" class="button button-secondary" data-command="powerBatCmd" data-value="10H">启动</button>
|
|
<button type="button" class="button button-secondary" data-command="powerBatCmd" data-value="20H">关闭</button>
|
|
</div>
|
|
<input type="hidden" name="powerBatCmd" value="00H">
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">动力锂电池功率配置值 (0~400 kW)</label>
|
|
<div class="slider-container">
|
|
<input type="range" class="slider" name="powerBatPower" min="0" max="400" value="0" step="10">
|
|
<span class="slider-value">0 kW</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="control-label">主机状态</label>
|
|
<div class="radio-group">
|
|
<label class="radio-option"><input type="radio" name="hostStatus" value="AAH" checked><span>运行</span></label>
|
|
<label class="radio-option"><input type="radio" name="hostStatus" value="FFH"><span>关机</span></label>
|
|
<label class="radio-option"><input type="radio" name="hostStatus" value="55H"><span>故障</span></label>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="button form-submit">提交操控指令</button>
|
|
<div id="statusMessage" class="status-message"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let ws = null;
|
|
|
|
function getFCModeText(mode) {
|
|
const modes = { 0: '无效', 1: '调试', 2: '自动', 3: '补给' };
|
|
return modes[mode] || '未知';
|
|
}
|
|
|
|
function getFCStatusText(status) {
|
|
const statuses = {
|
|
0: '无效', 1: '初始化', 2: '待机', 3: '自检异常', 4: '就绪',
|
|
5: '启动', 6: '运行', 7: '停机', 8: '故障', 9: '补给',
|
|
10: '制氢预热', 11: '制氢预热完成', 12: '停机维护', 13: '维护完成'
|
|
};
|
|
return statuses[status] || '未知';
|
|
}
|
|
|
|
function getFaultLevelText(level) {
|
|
const levels = { 0: '无故障', 1: '一级故障', 2: '二级故障', 3: '三级故障', 4: '四级故障' };
|
|
return levels[level] || '未知';
|
|
}
|
|
|
|
function getFlameText(state) {
|
|
const states = { 0: '无效', 1: '火焰报警', 2: '探测器故障' };
|
|
return states[state] || '未知';
|
|
}
|
|
|
|
function fmt(v, digits) {
|
|
if (v === undefined || v === null || isNaN(v)) return '--';
|
|
return Number(v).toFixed(digits === undefined ? 2 : digits);
|
|
}
|
|
|
|
function updateText(id, text) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.textContent = text;
|
|
}
|
|
|
|
function updateData(data) {
|
|
updateText('fc-mode', getFCModeText(data.fc_mode));
|
|
updateText('fc-status', getFCStatusText(data.fc_status));
|
|
updateText('fc-fault-level', getFaultLevelText(data.fc_fault_level));
|
|
updateText('fault-level-1', '0x' + (data.fc_fault_level_1 === undefined ? 0 : data.fc_fault_level_1).toString(16).toUpperCase().padStart(4, '0'));
|
|
updateText('fault-level-2', '0x' + (data.fc_fault_level_2 === undefined ? 0 : data.fc_fault_level_2).toString(16).toUpperCase().padStart(4, '0'));
|
|
updateText('fault-level-3', '0x' + (data.fc_fault_level_3 === undefined ? 0 : data.fc_fault_level_3).toString(16).toUpperCase().padStart(4, '0'));
|
|
updateText('fault-level-4', '0x' + (data.fc_fault_level_4 === undefined ? 0 : data.fc_fault_level_4).toString(16).toUpperCase().padStart(4, '0'));
|
|
updateText('total-gen-time', fmt(data.total_generation_time, 1) + ' h');
|
|
updateText('output-power-limit', fmt(data.output_power_limit, 0) + ' W');
|
|
updateText('total-gen-power', fmt(data.total_generation_power, 2) + ' kW');
|
|
updateText('hydrogen-capacity', fmt(data.hydrogen_capacity, 0) + ' %');
|
|
updateText('liquid-oxygen-capacity', fmt(data.liquid_oxygen_capacity, 0) + ' %');
|
|
updateText('heartbeat', data.heartbeat === undefined ? '--' : data.heartbeat);
|
|
updateText('emergency-command', data.emergency_command === undefined ? '--' : '0x' + data.emergency_command.toString(16).toUpperCase().padStart(2, '0'));
|
|
|
|
// FC单片电压
|
|
updateText('fc1-min-cell-v', fmt(data.fc1_min_cell_voltage, 0) + ' mV');
|
|
updateText('fc1-min-cell-pos', data.fc1_min_cell_pos === undefined ? '--' : data.fc1_min_cell_pos);
|
|
updateText('fc1-avg-cell-v', fmt(data.fc1_avg_cell_voltage, 0) + ' mV');
|
|
updateText('fc2-min-cell-v', fmt(data.fc2_min_cell_voltage, 0) + ' mV');
|
|
updateText('fc2-min-cell-pos', data.fc2_min_cell_pos === undefined ? '--' : data.fc2_min_cell_pos);
|
|
updateText('fc2-avg-cell-v', fmt(data.fc2_avg_cell_voltage, 0) + ' mV');
|
|
|
|
// 甲醇制氢装置
|
|
updateText('palladium-temp', fmt(data.palladium_temp, 1) + ' ℃');
|
|
updateText('buffer-tank-pressure', fmt(data.buffer_tank_pressure, 1) + ' kPa');
|
|
updateText('flue-total-emission', fmt(data.flue_total_emission, 1) + ' kg');
|
|
updateText('flue-pressure', fmt(data.flue_pressure, 3) + ' MPa');
|
|
updateText('reactor-pressure', fmt(data.reactor_pressure, 3) + ' MPa');
|
|
updateText('electric-valve-open', fmt(data.electric_valve_open, 1) + ' %');
|
|
|
|
// DC/DC
|
|
updateText('dcdc1-in-voltage', fmt(data.dcdc1_in_voltage, 1) + ' V');
|
|
updateText('dcdc1-in-current', fmt(data.dcdc1_in_current, 1) + ' A');
|
|
updateText('dcdc2-in-voltage', fmt(data.dcdc2_in_voltage, 1) + ' V');
|
|
updateText('dcdc2-in-current', fmt(data.dcdc2_in_current, 1) + ' A');
|
|
updateText('dcdc-out-voltage', fmt(data.dcdc_out_voltage, 1) + ' V');
|
|
updateText('dcdc-out-current', fmt(data.dcdc_out_current, 1) + ' A');
|
|
updateText('dcdc-control-voltage', fmt(data.dcdc_control_voltage, 2) + ' V');
|
|
updateText('dcdc-aux-out-voltage', fmt(data.dcdc_aux_out_voltage, 3) + ' V');
|
|
|
|
// 甲醇/水系统
|
|
updateText('methanol-total-usage', fmt(data.methanol_total_usage, 1) + ' kg');
|
|
updateText('methanol-solution-feed', fmt(data.methanol_solution_feed, 0) + ' mL/min');
|
|
updateText('oxygen-water-tank-level', fmt(data.oxygen_water_tank_level, 1) + ' mm');
|
|
updateText('hydrogen-water-tank-level', fmt(data.hydrogen_water_tank_level, 1) + ' mm');
|
|
updateText('ballast-water-tank-level', fmt(data.ballast_water_tank_level, 1) + ' mm');
|
|
|
|
// 尾气装置
|
|
updateText('exhaust-in-pressure', fmt(data.exhaust_in_pressure, 2) + ' MPa');
|
|
updateText('exhaust-out-pressure', fmt(data.exhaust_out_pressure, 2) + ' MPa');
|
|
updateText('exhaust-run-freq', fmt(data.exhaust_run_freq, 2) + ' Hz');
|
|
updateText('exhaust-in-temp', fmt(data.exhaust_in_temp, 2) + ' ℃');
|
|
updateText('exhaust-out-temp', fmt(data.exhaust_out_temp, 2) + ' ℃');
|
|
updateText('exhaust-in-water-pressure', fmt(data.exhaust_in_water_pressure, 2) + ' kPa');
|
|
updateText('exhaust-out-water-pressure', fmt(data.exhaust_out_water_pressure, 2) + ' kPa');
|
|
|
|
// 一体化罐装置
|
|
updateText('tank-lo2-pressure', fmt(data.tank_lo2_pressure, 2) + ' MPa');
|
|
updateText('tank-co2-pressure', fmt(data.tank_co2_pressure, 2) + ' MPa');
|
|
updateText('tank-lo2-level', fmt(data.tank_lo2_level, 1) + ' mm');
|
|
|
|
// 供氢/供氧流量
|
|
updateText('alloy-h2-flow', fmt(data.alloy_h2_flow, 2) + ' L/min');
|
|
updateText('fc-h2-flow', fmt(data.fc_h2_flow, 2) + ' L/min');
|
|
updateText('fc-o2-flow', fmt(data.fc_o2_flow, 2) + ' L/min');
|
|
|
|
// 应急上浮
|
|
updateText('emergency-float-depth', fmt(data.emergency_float_depth, 0) + ' m');
|
|
updateText('emergency-float-time', fmt(data.emergency_float_time, 0) + ' min');
|
|
|
|
// 舱室环境
|
|
updateText('cabin-pressure1', fmt(data.cabin_pressure1, 2) + ' kPa');
|
|
updateText('cabin-pressure2', fmt(data.cabin_pressure2, 2) + ' kPa');
|
|
updateText('cabin-temp1', fmt(data.cabin_temp1, 2) + ' ℃');
|
|
updateText('cabin-temp2', fmt(data.cabin_temp2, 2) + ' ℃');
|
|
updateText('cabin-humidity1', fmt(data.cabin_humidity1, 2) + ' %RH');
|
|
updateText('cabin-humidity2', fmt(data.cabin_humidity2, 2) + ' %RH');
|
|
updateText('h2-concentration1', fmt(data.h2_concentration1, 2) + ' %LEL');
|
|
updateText('h2-concentration2', fmt(data.h2_concentration2, 2) + ' %LEL');
|
|
updateText('h2-concentration3', fmt(data.h2_concentration3, 2) + ' %LEL');
|
|
updateText('o2-concentration1', fmt(data.o2_concentration1, 2) + ' %Vol');
|
|
updateText('o2-concentration2', fmt(data.o2_concentration2, 2) + ' %Vol');
|
|
updateText('ch3oh-concentration1', fmt(data.ch3oh_concentration1, 2) + ' %LEL');
|
|
updateText('ch3oh-concentration2', fmt(data.ch3oh_concentration2, 2) + ' %LEL');
|
|
updateText('flame-detector1', getFlameText(data.flame_detector1));
|
|
updateText('flame-detector2', getFlameText(data.flame_detector2));
|
|
|
|
// 通信状态
|
|
const timeout = data.comm_state && data.comm_state.ccu_status ? data.comm_state.ccu_status.isTimeout : true;
|
|
const commEl = document.getElementById('commState');
|
|
if (timeout) {
|
|
commEl.textContent = '通讯中断';
|
|
commEl.className = 'badge off';
|
|
} else {
|
|
commEl.textContent = '通讯正常';
|
|
commEl.className = 'badge on';
|
|
}
|
|
}
|
|
|
|
function initWebSocket() {
|
|
ws = new WebSocket('ws://' + window.location.host + '/ws');
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
// 忽略日志推送消息(type=new_log/log),只处理系统数据推送
|
|
if (data.type === 'new_log' || data.type === 'log') return;
|
|
if (data.fc_mode === undefined) return;
|
|
updateData(data);
|
|
} catch (e) {
|
|
console.error('JSON parse error', e);
|
|
}
|
|
};
|
|
ws.onclose = () => {
|
|
setTimeout(initWebSocket, 5000);
|
|
};
|
|
ws.onerror = (err) => {
|
|
console.error('WebSocket error', err);
|
|
};
|
|
}
|
|
|
|
// 更新滑块值显示
|
|
document.querySelectorAll('.slider').forEach(slider => {
|
|
const valueDisplay = slider.nextElementSibling;
|
|
if (slider.name === 'fcPower') {
|
|
valueDisplay.textContent = slider.value + ' (0.0 kW)';
|
|
} else if (slider.name === 'depth') {
|
|
valueDisplay.textContent = slider.value + ' m';
|
|
} else if (slider.name === 'powerBatPower') {
|
|
valueDisplay.textContent = slider.value + ' kW';
|
|
} else {
|
|
valueDisplay.textContent = slider.value;
|
|
}
|
|
|
|
slider.addEventListener('input', () => {
|
|
if (slider.name === 'fcPower') {
|
|
valueDisplay.textContent = slider.value + ' (' + (slider.value * 0.1).toFixed(1) + ' kW)';
|
|
} else if (slider.name === 'depth') {
|
|
valueDisplay.textContent = slider.value + ' m';
|
|
} else if (slider.name === 'powerBatPower') {
|
|
valueDisplay.textContent = slider.value + ' kW';
|
|
} else {
|
|
valueDisplay.textContent = slider.value;
|
|
}
|
|
});
|
|
});
|
|
|
|
// 处理按钮组选择
|
|
document.querySelectorAll('.button-group button').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const group = button.closest('.button-group');
|
|
const hiddenInput = group.nextElementSibling;
|
|
const command = button.dataset.command;
|
|
const value = button.dataset.value;
|
|
|
|
group.querySelectorAll('button').forEach(btn => {
|
|
btn.classList.remove('button-active');
|
|
btn.classList.add('button-secondary');
|
|
});
|
|
button.classList.remove('button-secondary');
|
|
button.classList.add('button-active');
|
|
|
|
hiddenInput.value = value;
|
|
});
|
|
});
|
|
|
|
// 处理表单提交
|
|
document.getElementById('fcsForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const formData = new FormData(form);
|
|
const statusMessage = document.getElementById('statusMessage');
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
for (const [key, value] of formData.entries()) {
|
|
params.append(key, value);
|
|
}
|
|
|
|
const response = await fetch(form.action, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: params.toString()
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
statusMessage.textContent = '操控指令提交成功';
|
|
statusMessage.className = 'status-message';
|
|
statusMessage.style.display = 'block';
|
|
} else {
|
|
statusMessage.textContent = result.message || '操控指令提交失败';
|
|
statusMessage.className = 'status-message error';
|
|
statusMessage.style.display = 'block';
|
|
}
|
|
} catch (error) {
|
|
statusMessage.textContent = '网络错误: ' + error.message;
|
|
statusMessage.className = 'status-message error';
|
|
statusMessage.style.display = 'block';
|
|
}
|
|
|
|
setTimeout(() => {
|
|
statusMessage.style.display = 'none';
|
|
}, 5000);
|
|
});
|
|
|
|
window.addEventListener('load', initWebSocket);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
)";
|
|
|
|
#endif // HTTP_SERVER_FUELCELL_H
|