XNSim/XNSimHtml/components/model-monitor.js

637 lines
22 KiB
JavaScript
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.

class ModelMonitor extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.monitorStatus = {
isMonitoring: false,
lastError: null
};
this.modelInfo = null;
this.statusCheckInterval = null;
this.chart = null;
this.chartInitialized = false;
}
connectedCallback() {
this.render();
// 等待组件完全加载
setTimeout(() => {
this.initializeComponent();
}, 100);
}
initializeComponent() {
if (this.chartInitialized) return;
try {
this.initChart();
this.startStatusCheck();
this.chartInitialized = true;
} catch (error) {
console.error('初始化组件失败:', error);
}
}
disconnectedCallback() {
this.stopStatusCheck();
}
startStatusCheck() {
this.checkMonitorStatus();
this.statusCheckInterval = setInterval(() => {
this.checkMonitorStatus();
}, 1000);
}
stopStatusCheck() {
if (this.statusCheckInterval) {
clearInterval(this.statusCheckInterval);
this.statusCheckInterval = null;
}
}
async checkMonitorStatus() {
try {
// 获取监控状态
const statusResponse = await fetch('/api/model-monitor/status');
const statusData = await statusResponse.json();
this.monitorStatus = statusData;
if (this.monitorStatus.isMonitoring) {
try {
// 获取模型信息
const modelResponse = await fetch('/api/model-monitor/model-info');
const modelData = await modelResponse.json();
if (!modelResponse.ok) {
console.error('获取模型信息失败:', modelData.error || modelData.message);
this.monitorStatus.lastError = modelData.error || modelData.message;
this.modelInfo = null;
} else {
this.modelInfo = modelData.data;
this.monitorStatus.lastError = null;
}
} catch (error) {
console.error('获取模型信息失败:', error);
this.monitorStatus.lastError = error.message;
this.modelInfo = null;
}
} else {
this.modelInfo = null;
this.monitorStatus.lastError = null;
}
this.updateUI();
} catch (error) {
console.error('获取监控状态失败:', error);
this.monitorStatus.lastError = error.message;
this.modelInfo = null;
this.updateUI();
}
}
getStatusDisplay(status) {
switch (status) {
case 0:
return { text: '未运行', color: '#999999' };
case 1:
return { text: '运行中', color: '#4CAF50' };
case 2:
return { text: '暂停中', color: '#FF9800' };
case 3:
return { text: '错误', color: '#f44336' };
default:
return { text: '未知状态', color: '#f44336' };
}
}
updateUI() {
const input = this.shadowRoot.querySelector('.domain-input');
const startButton = this.shadowRoot.querySelector('.start-button');
const stopButton = this.shadowRoot.querySelector('.stop-button');
const statusDisplay = this.shadowRoot.querySelector('.status-display');
const modelTableBody = this.shadowRoot.querySelector('#model-table-body');
if (this.monitorStatus.isMonitoring) {
input.disabled = true;
startButton.disabled = true;
stopButton.disabled = false;
} else {
input.disabled = false;
startButton.disabled = false;
stopButton.disabled = true;
}
// 更新状态显示
statusDisplay.textContent = `监控状态: ${this.monitorStatus.isMonitoring ? '运行中' : '已停止'}`;
if (this.monitorStatus.lastError) {
statusDisplay.textContent += ` (错误: ${this.monitorStatus.lastError})`;
}
// 更新模型表格
if (this.monitorStatus.isMonitoring && this.modelInfo && Array.isArray(this.modelInfo)) {
modelTableBody.innerHTML = this.modelInfo.map(model => {
const statusInfo = this.getStatusDisplay(model.status);
return `
<tr>
<td>${model.name || '未知'}</td>
<td>${model.id || '未知'}</td>
<td style="color: ${statusInfo.color}">${statusInfo.text}</td>
<td>${model.threadId || '未知'}</td>
<td>${model.node}</td>
<td>${model.priority || '未知'}</td>
<td>${model.runCount || '0'}</td>
<td>${(model.setFrequency || 0).toFixed(2)}</td>
<td>${(model.avgFrequency || 0).toFixed(2)}</td>
<td>${(model.maxFrequency || 0).toFixed(2)}</td>
<td>${(model.minFrequency || 0).toFixed(2)}</td>
<td>${(model.setPeriod || 0).toFixed(2)}</td>
<td>${(model.avgPeriod || 0).toFixed(2)}</td>
<td>${(model.maxPeriod || 0).toFixed(2)}</td>
<td>${(model.minPeriod || 0).toFixed(2)}</td>
</tr>
`;
}).join('');
// 更新图表数据
this.updateChartData();
} else {
modelTableBody.innerHTML = '<tr><td colspan="14" style="text-align: center;">暂无模型信息</td></tr>';
// 清空图表数据
if (this.chart) {
this.chart.data.labels = [];
this.chart.data.datasets = [];
this.chart.update();
}
}
}
async startMonitoring() {
const domainId = this.shadowRoot.querySelector('.domain-input').value.trim();
// 验证域ID是否为有效的数字字符串
if (!/^\d+$/.test(domainId)) {
console.error('域ID必须是有效的数字');
return;
}
try {
// 首先检查DDS监控状态
const ddsStatusResponse = await fetch('/api/dds-monitor/status');
const ddsStatusData = await ddsStatusResponse.json();
// 如果DDS监控未初始化先初始化DDS监控
if (!ddsStatusData.isInitialized) {
const ddsInitResponse = await fetch('/api/dds-monitor/initialize', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domainId })
});
if (!ddsInitResponse.ok) {
const errorData = await ddsInitResponse.json();
console.error('DDS监控初始化失败:', errorData.error);
return;
}
}
// 启动模型监控
const response = await fetch('/api/model-monitor/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domainId })
});
const data = await response.json();
if (response.ok) {
this.monitorStatus = data.status;
this.updateUI();
} else {
console.error('启动监控失败:', data.error);
}
} catch (error) {
console.error('启动监控失败:', error);
}
}
async stopMonitoring() {
try {
const response = await fetch('/api/model-monitor/stop', {
method: 'POST'
});
const data = await response.json();
if (response.ok) {
this.monitorStatus = data.status;
this.updateUI();
} else {
console.error('停止监控失败:', data.error);
}
} catch (error) {
console.error('停止监控失败:', error);
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
.monitor-container {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
box-sizing: border-box;
}
.toolbar-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
}
.content-container {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
}
.panel-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
}
.toolbar {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
}
.toolbar-left {
display: flex;
gap: 12px;
align-items: center;
}
.input-group {
display: flex;
align-items: center;
gap: 8px;
}
.input-label {
font-size: 14px;
color: #333;
white-space: nowrap;
}
.domain-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 200px;
}
.domain-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.control-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.control-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.start-button {
background-color: #4CAF50;
color: white;
}
.start-button:hover:not(:disabled) {
background-color: #45a049;
}
.stop-button {
background-color: #f44336;
color: white;
}
.stop-button:hover:not(:disabled) {
background-color: #da190b;
}
.status-display {
padding: 8px 12px;
background-color: #f5f5f5;
border-radius: 4px;
font-size: 14px;
color: #666;
}
h3 {
margin: 0 0 12px 0;
color: #333;
font-size: 16px;
}
.model-table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
}
.model-table th,
.model-table td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.model-table th {
background-color: #f8f9fa;
font-weight: 500;
color: #333;
}
.model-table tr:hover {
background-color: #f8f9fa;
}
.chart-container {
position: relative;
width: 100%;
height: 300px;
margin: 0;
padding: 0;
}
#model-chart {
width: 100% !important;
height: 100% !important;
display: block;
}
.chart-controls {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
</style>
<div class="monitor-container">
<div class="toolbar-section">
<div class="toolbar">
<div class="toolbar-left">
<div class="input-group">
<label class="input-label">DDS通信域ID</label>
<input type="text" class="domain-input" value="10">
</div>
<button class="control-button start-button" onclick="this.getRootNode().host.startMonitoring()">开始监控</button>
<button class="control-button stop-button" onclick="this.getRootNode().host.stopMonitoring()">停止监控</button>
</div>
<div class="status-display">监控状态: 未启动</div>
</div>
</div>
<div class="content-container">
<div class="panel-section">
<h3>模型信息</h3>
<table class="model-table">
<thead>
<tr>
<th>模型名称</th>
<th>模型ID</th>
<th>状态</th>
<th>线程ID</th>
<th>节点</th>
<th>优先级</th>
<th>运行次数</th>
<th>设定频率(Hz)</th>
<th>平均频率(Hz)</th>
<th>最大频率(Hz)</th>
<th>最小频率(Hz)</th>
<th>设定周期(ms)</th>
<th>平均周期(ms)</th>
<th>最大周期(ms)</th>
<th>最小周期(ms)</th>
</tr>
</thead>
<tbody id="model-table-body"></tbody>
</table>
</div>
<div class="panel-section">
<h3>模型监控</h3>
<div class="chart-container">
<canvas id="model-chart"></canvas>
</div>
<div class="chart-controls">
<span>显示类型:</span>
<label class="switch">
<input type="checkbox" id="display-type-switch">
<span class="slider"></span>
</label>
<span id="display-type-label">频率</span>
</div>
</div>
</div>
</div>
`;
}
initChart() {
const chartElement = this.shadowRoot.querySelector('#model-chart');
if (!chartElement) {
console.error('找不到图表元素');
return;
}
// 确保 Chart.js 已加载
if (typeof Chart === 'undefined') {
console.error('Chart.js 未加载');
return;
}
try {
// 创建图表实例
const ctx = chartElement.getContext('2d');
if (!ctx) {
console.error('无法获取 canvas 上下文');
return;
}
// 销毁已存在的图表实例
if (this.chart) {
this.chart.destroy();
}
// 创建新的图表实例
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: []
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
},
animation: false,
plugins: {
legend: {
display: true
},
tooltip: {
enabled: false
}
}
}
});
// 监听显示类型切换
const switchInput = this.shadowRoot.querySelector('#display-type-switch');
const displayTypeLabel = this.shadowRoot.querySelector('#display-type-label');
if (switchInput && displayTypeLabel) {
switchInput.addEventListener('change', (e) => {
const isFrequency = e.target.checked;
displayTypeLabel.textContent = isFrequency ? '频率' : '周期';
this.updateChartDisplayType(isFrequency);
});
}
} catch (error) {
console.error('初始化图表失败:', error);
this.chart = null;
}
}
updateChartData() {
if (!this.chart || !this.modelInfo || !Array.isArray(this.modelInfo)) return;
const isFrequency = this.shadowRoot.querySelector('#display-type-switch').checked;
const currentTime = new Date().toLocaleTimeString();
// 确保图表数据集存在
if (!this.chart.data.datasets || this.chart.data.datasets.length === 0) {
this.updateChartDisplayType(isFrequency);
}
// 更新数据
const newData = {
labels: [...this.chart.data.labels, currentTime].slice(-30),
datasets: this.modelInfo.map((model, index) => {
const dataset = this.chart.data.datasets[index] || {
label: model.name,
data: [],
borderColor: this.getRandomColor(),
fill: false
};
const value = isFrequency ? (model.currentFrequency || 0) : (model.currentPeriod || 0);
dataset.data = [...dataset.data, value].slice(-30);
return dataset;
})
};
// 使用新数据更新图表
this.chart.data = newData;
this.chart.update('none'); // 使用 'none' 模式更新,避免触发动画和事件
}
updateChartDisplayType(isFrequency) {
if (!this.modelInfo) return;
const datasets = this.modelInfo.map(model => ({
label: model.name,
data: [],
borderColor: this.getRandomColor(),
fill: false
}));
this.chart.data.datasets = datasets;
this.chart.update('none'); // 使用 'none' 模式更新
}
getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
}
customElements.define('model-monitor', ModelMonitor);