Files
H100PowerManger/src/pPowerManger/httpserver/fuelcell.h
T
zjk e0488d513b 协议:按0824协议更新复合管控通信解析 + 新增燃料电池监控网页
- pmSysvariable/ccuUdpMsg: 域起始符改为0x40 0x40,域标识符重编号0x0001~0x0004,
  ccuState/ccuColCmd/ccuSetParmCmd 按新协议字段布局与字节序重写(状态248B/操控45B/参数28B)
- systemData/httpserver: 新增FC单片电压、甲醇制氢装置、DC/DC、尾气装置、一体化罐、
  供氢供氧流量、应急上浮等字段解析与WebSocket JSON推送
- 新增 /fcs 燃料电池监控页(fuelcell.h):实时数据显示 + 操控指令交互按钮,
  过滤日志推送消息避免界面闪烁
- SQLite/LowerCommManager/测试:更新建表列、解析打印与单元/集成测试适配新协议
2026-08-26 20:56:39 +08:00

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