当前位置: 首页 > news >正文

20、工业协议转换与数据采集中间件 (模拟) - /数据与物联网组件/protocol-converter-middleware

76个工业组件库示例汇总

工业协议转换与数据采集中间件

概述

这是一个交互式的 Web 组件,旨在模拟一个工业数据采集中间件的核心功能。该中间件的概念是连接到工厂中的多种不同品牌、不同协议的 PLC(可编程逻辑控制器),统一采集数据点(标签),并在一个集中的界面进行监控和记录日志。

请注意:这是一个高度简化的前端模拟演示,不涉及真实的协议转换或网络通信。所有 PLC 连接、数据值、状态变化和日志都是在浏览器端通过 JavaScript 模拟生成的。

主要功能

  • 连接管理:
    • 显示预设的多个 PLC 连接配置(模拟不同品牌如 Siemens, Rockwell, Mitsubishi 等及其常用协议 S7, EtherNet/IP, MELSEC 等)。
    • 实时展示每个连接的状态:DISCONNECTED (已断开), CONNECTING (连接中), CONNECTED (已连接), ERROR (错误)。状态会基于模拟逻辑动态变化。
    • 提供搜索功能,可以按连接名称、IP 地址或协议过滤连接列表。
    • (概念性) 包含一个"添加连接"按钮(当前禁用)。
  • 数据点监控:
    • 以表格形式集中展示从所有已连接的 PLC 中模拟采集到的数据点(标签)。
    • 表格列包括:标签名、当前值、来源 PLC、最新时间戳、数据质量状态。
    • 实时数据更新: 表格中数据点的值会定期模拟更新(数值型波动、布尔型随机翻转、字符串偶尔变化)。
    • 数据质量模拟: 数据质量状态 (GOOD, UNCERTAIN, BAD) 会模拟变化,例如连接断开或错误时质量变为 BAD,偶尔也会模拟 UNCERTAIN 状态。
    • 提供搜索功能,可以按标签名或当前值过滤表格内容。
    • 提供下拉菜单,可以按来源 PLC 过滤表格内容。
  • 事件日志:
    • 实时记录中间件运行过程中的关键事件。
    • 日志条目包含时间戳、日志级别(INFO, WARN, ERROR)和事件消息。
    • 记录的事件类型包括:中间件启动/初始化、尝试连接 PLC、连接成功/失败/断开、数据点添加、数据质量变化等。
    • 提供按日志级别 (INFO, WARN, ERROR) 过滤显示日志的功能。
    • 提供清除当前显示日志的功能。
  • 全局状态: 页眉区域显示中间件的整体连接状态(如:全部已连接、部分连接、连接中、错误、已断开)和状态指示灯。
  • 界面风格: 采用苹果科技风格,三栏布局,简洁清晰,支持响应式设计。

如何使用

  1. 打开页面: 在浏览器中打开 index.html
  2. 观察连接: 左侧"连接管理"面板显示模拟的 PLC 连接及其状态。观察状态指示灯和文字的变化(如从 DISCONNECTED 变为 CONNECTING,然后变为 CONNECTEDERROR)。
  3. 监控数据: 中间的"数据点监控"表格会显示从状态为 CONNECTED 的 PLC 中模拟读取的数据点。观察值的实时变化以及质量状态的变化。
  4. 过滤/搜索数据:
    • 在数据点表格上方的搜索框输入标签名或值的关键字进行过滤。
    • 使用下拉菜单选择特定的 PLC 名称,只显示该来源的数据点。
  5. 查看日志: 右侧"事件日志"面板实时滚动显示中间件的操作记录。
  6. 过滤日志: 点击日志面板上方的复选框 (INFO, WARN, ERROR) 来选择要显示的日志级别。
  7. 清除日志: 点击"清除日志"按钮可以清空当前显示的日志记录(内存中的日志条数有上限,旧日志会自动移除)。
  8. 搜索连接: 在连接管理面板的搜索框输入关键字过滤连接列表。

模拟细节

  • 连接配置: 组件初始化时会生成一组模拟的 PLC 连接,随机分配品牌、协议和 IP 地址,并关联一组模拟的数据点标签定义。
  • 连接状态转换:
    • DISCONNECTED 状态下,有一定几率尝试变为 CONNECTING
    • CONNECTING 状态下,有较大概率变为 CONNECTED,小概率变为 ERROR(模拟连接失败),否则保持 CONNECTING
    • CONNECTED 状态下,有很小几率变为 DISCONNECTED(模拟断线)或 ERROR(模拟通信异常)。
    • ERROR 状态下,有一定几率恢复为 DISCONNECTED(准备重试)。
  • 数据点生成: 当一个连接状态变为 CONNECTED 时,其关联的标签会被添加到中央数据点列表中,并赋予初始值和 GOOD 质量。
  • 数据点更新: 对于状态为 CONNECTED 的 PLC,其中间件会定期模拟更新其关联数据点的值。数值会小幅波动,布尔值有低概率翻转,字符串偶尔变化。
  • 数据质量模拟:
    • 连接断开 (DISCONNECTED) 或发生错误 (ERROR) 时,其对应的数据点质量会变为 BADUNCERTAIN,值显示为 #BAD#UNCERTAIN
    • 即使连接正常,数据点也有极小概率随机变为 UNCERTAINBAD(模拟读取失败或校验错误)。
    • 质量非 GOOD 的数据点也有一定概率恢复为 GOOD
  • 日志: 在状态转换、数据点添加/移除(或质量变化)、用户操作(清除日志)等时刻生成对应级别的日志条目。

文件结构

数据与物联网组件/protocol-converter-middleware/
├── index.html         # 组件的 HTML 结构
├── styles.css         # 组件的 CSS 样式 (苹果风格, 三栏响应式)
├── script.js          # 组件的 JavaScript 逻辑 (模拟连接, 数据更新, 日志)
└── README.md          # 本说明文件

技术栈

  • HTML5
  • CSS3 (使用了 CSS 变量, Grid 布局, Flexbox, 动画, 媒体查询)
  • JavaScript (原生 JS, 无外部库依赖)

效果展示

在这里插入图片描述

源码

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>工业协议转换与数据采集中间件</title><link rel="stylesheet" href="styles.css">
</head>
<body><div class="middleware-container"><header class="main-header"><h1>工业协议转换与数据采集中间件 (模拟)</h1><div class="header-status"><span class="status-indicator" id="globalStatusIndicator"></span><span id="globalStatusText">初始化中...</span></div></header><main class="main-content"><!-- Column 1: Connection Management --><section class="connection-panel panel"><h2><i class="icon icon-connect"></i> 连接管理</h2><div class="panel-toolbar"><button class="action-button add-btn" disabled title="功能暂未实现"><i class="icon icon-plus"></i> 添加连接</button><input type="search" id="connectionSearch" placeholder="搜索连接..."></div><ul id="connectionList" class="connection-list"><li class="placeholder">加载连接配置中...</li><!-- Connection items populated by JS --></ul></section><!-- Column 2: Data Point Monitoring --><section class="data-panel panel"><h2><i class="icon icon-tags"></i> 数据点监控 (<span id="monitoredPointsCount">0</span>)</h2><div class="panel-toolbar"><input type="search" id="dataPointSearch" placeholder="搜索标签名或值..."><select id="dataSourceFilter" title="按数据源过滤"><option value="all">所有源</option><!-- Options populated by JS --></select></div><div class="data-point-table-container"><table id="dataPointTable"><thead><tr><th>标签名</th><th>当前值</th><th>来源PLC</th><th>时间戳</th><th>状态</th></tr></thead><tbody id="dataPointTBody"><tr class="placeholder-row"><td colspan="5">等待数据接入...</td></tr><!-- Data rows populated by JS --></tbody></table></div></section><!-- Column 3: Event Log --><section class="log-panel panel"><h2><i class="icon icon-log"></i> 事件日志</h2><div class="panel-toolbar"><button class="action-button clear-log-btn" id="clearLogBtn"><i class="icon icon-clear"></i> 清除日志</button><span class="log-level-filter"><label><input type="checkbox" name="logLevel" value="INFO" checked> INFO</label><label><input type="checkbox" name="logLevel" value="WARN" checked> WARN</label><label><input type="checkbox" name="logLevel" value="ERROR" checked> ERROR</label></span></div><ul id="logList" class="log-list"><li class="log-item info placeholder">中间件启动</li><!-- Log items populated by JS --></ul></section></main><footer class="main-footer"><p>&copy; 2024 工业中间件模拟系统. 概念演示.</p></footer></div><script src="script.js"></script>
</body>
</html> 

styles.css

:root {--bg-color-light: #f9f9f9;--bg-color-container: #ffffff;--header-bg: #f5f5f7;--panel-bg: #ffffff;--border-color: #e1e1e1;--text-primary: #1d1d1f;--text-secondary: #515154;--text-label: #6e6e73;--accent-blue: #007aff;--accent-green: #34c759;--accent-orange: #ff9500;--accent-red: #ff3b30;--accent-grey: #8e8e93;--status-connecting: var(--accent-blue);--status-connected: var(--accent-green);--status-error: var(--accent-red);--status-disconnected: var(--accent-grey);--status-quality-good: var(--accent-green);--status-quality-uncertain: var(--accent-orange);--status-quality-bad: var(--accent-red);--log-info: var(--accent-blue);--log-warn: var(--accent-orange);--log-error: var(--accent-red);--list-item-hover-bg: #f0f0f0;--list-item-selected-bg: #e8f3ff; /* Keep selection subtle */--list-item-selected-text: var(--text-primary);--input-bg: #f0f2f5;--input-border: transparent;--input-focus-border: var(--accent-blue);--placeholder-text: #aaaaaa;--table-header-bg: #f9f9f9;--table-row-hover-bg: #f5faff;--shadow-color: rgba(0, 0, 0, 0.05);--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-monospace: "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--border-radius: 8px;--border-radius-small: 4px;--transition-speed: 0.2s;
}* {box-sizing: border-box;margin: 0;padding: 0;
}body {font-family: var(--font-family);background-color: var(--bg-color-light);color: var(--text-primary);line-height: 1.4;overflow-x: hidden;
}.middleware-container {max-width: 1800px;margin: 1rem auto;background-color: var(--bg-color-container);border-radius: var(--border-radius);box-shadow: 0 4px 12px var(--shadow-color);overflow: hidden;display: flex;flex-direction: column;height: calc(100vh - 2rem); /* Limit height */min-height: 650px; /* Minimum reasonable height */
}/* Header */
.main-header {background-color: var(--header-bg);padding: 0.75rem 1.5rem;border-bottom: 1px solid var(--border-color);flex-shrink: 0;display: flex;justify-content: space-between;align-items: center;
}.main-header h1 {font-size: 1.3rem;font-weight: 600;color: var(--text-primary);
}.header-status {display: flex;align-items: center;gap: 0.5rem;font-size: 0.9rem;color: var(--text-secondary);
}.status-indicator {width: 12px;height: 12px;border-radius: 50%;background-color: var(--status-disconnected);transition: background-color var(--transition-speed);animation: pulse-grey 2s infinite ease-in-out;
}.status-indicator.connecting {background-color: var(--status-connecting);animation: pulse-blue 1.5s infinite ease-in-out;
}.status-indicator.connected {background-color: var(--status-connected);animation: none; /* Solid green when connected */
}.status-indicator.error {background-color: var(--status-error);animation: pulse-red 1s infinite ease-in-out;
}@keyframes pulse-blue {0%, 100% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0.4); }50% { box-shadow: 0 0 0 5px rgba(0, 122, 255, 0); }
}@keyframes pulse-red {0%, 100% { box-shadow: 0 0 0 0 rgba(255, 59, 48, 0.5); }50% { box-shadow: 0 0 0 5px rgba(255, 59, 48, 0); }
}@keyframes pulse-grey {0%, 100% { opacity: 0.6; }50% { opacity: 1; }
}/* Main Content Layout */
.main-content {flex-grow: 1;padding: 1rem;overflow: hidden;display: grid;grid-template-columns: 320px 1fr 450px; /* Connections | Data | Log */gap: 1rem;
}/* Panels */
.panel {background-color: var(--panel-bg);border-radius: var(--border-radius);padding: 1rem;display: flex;flex-direction: column;overflow: hidden;/* Optional subtle border *//* border: 1px solid var(--border-color); */
}.panel h2 {font-size: 1.1rem;font-weight: 600;margin-bottom: 1rem;padding-bottom: 0.6rem;border-bottom: 1px solid var(--border-color);display: flex;align-items: center;color: var(--text-primary);flex-shrink: 0;
}.panel h2 .icon {margin-right: 0.6rem;color: var(--accent-blue); /* Default icon color */
}.panel h2 span {font-weight: normal;font-size: 0.9em;color: var(--text-secondary);margin-left: 0.3rem;
}.panel-toolbar {margin-bottom: 0.75rem;display: flex;gap: 0.5rem;align-items: center;flex-shrink: 0;flex-wrap: wrap; /* Allow wrapping on smaller screens if needed */
}/* Common Input/Button Styles */
input[type="search"],
select {padding: 0.5rem 0.75rem;font-size: 0.9rem;border: 1px solid var(--input-border);background-color: var(--input-bg);border-radius: var(--border-radius-small);outline: none;transition: border-color var(--transition-speed), box-shadow var(--transition-speed);color: var(--text-primary);height: 34px; /* Match button height */
}input[type="search"] {flex-grow: 1; /* Allow search to take space */
}input[type="search"]:focus,
select:focus {border-color: var(--input-focus-border);box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}select {appearance: none;background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236e6e73'%3E%3Cpath fill-rule='evenodd' d='M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z'/%3E%3C/svg%3E");background-repeat: no-repeat;background-position: right 0.5rem center;background-size: 16px 16px;padding-right: 2rem; /* Make space for arrow */
}.action-button {padding: 0 0.75rem;height: 34px;font-size: 0.9rem;border: 1px solid var(--border-color);background-color: #fff;color: var(--accent-blue);border-radius: var(--border-radius-small);cursor: pointer;transition: background-color var(--transition-speed), border-color var(--transition-speed);display: inline-flex;align-items: center;gap: 0.4rem;white-space: nowrap;
}.action-button:hover {background-color: #f8f8f8;border-color: #d1d1d1;
}.action-button:active {background-color: #f0f0f0;
}.action-button:disabled {color: var(--accent-grey);cursor: not-allowed;background-color: #f5f5f5;opacity: 0.7;
}.action-button .icon {font-size: 1.1em;
}/* Connection Panel */
.connection-list {list-style: none;overflow-y: auto;flex-grow: 1;
}.connection-item {padding: 0.75rem 0.5rem;margin-bottom: 0.2rem;border-radius: var(--border-radius-small);cursor: default; /* Not selectable for now */transition: background-color var(--transition-speed);display: flex;justify-content: space-between;align-items: center;font-size: 0.9rem;border-bottom: 1px solid #f0f0f0;
}.connection-item:last-child {border-bottom: none;
}/* .connection-item:hover { background-color: var(--list-item-hover-bg); } */.connection-details {display: flex;flex-direction: column;flex-grow: 1;margin-right: 0.5rem;
}.connection-name {font-weight: 500;color: var(--text-primary);
}.connection-name .protocol {font-weight: normal;font-size: 0.8em;color: var(--text-label);margin-left: 0.3rem;background-color: #eee;padding: 0.1rem 0.3rem;border-radius: 3px;
}.connection-info {font-size: 0.8rem;color: var(--text-secondary);
}.connection-status {display: flex;align-items: center;gap: 0.4rem;font-size: 0.8rem;font-weight: 500;padding: 0.2rem 0.5rem;border-radius: var(--border-radius-small);flex-shrink: 0;min-width: 90px; /* Ensure consistent width */justify-content: center;
}.connection-status .dot {width: 8px;height: 8px;border-radius: 50%;display: inline-block;
}.connection-status.connecting {color: var(--status-connecting);background-color: rgba(0, 122, 255, 0.1);
}
.connection-status.connecting .dot { background-color: var(--status-connecting); }.connection-status.connected {color: var(--status-connected);background-color: rgba(52, 199, 89, 0.1);
}
.connection-status.connected .dot { background-color: var(--status-connected); }.connection-status.error {color: var(--status-error);background-color: rgba(255, 59, 48, 0.1);
}
.connection-status.error .dot { background-color: var(--status-error); }.connection-status.disconnected {color: var(--status-disconnected);background-color: rgba(142, 142, 147, 0.1);
}
.connection-status.disconnected .dot { background-color: var(--status-disconnected); }/* Data Point Panel */
.data-panel {grid-column: span 1; /* Takes middle space */
}.data-point-table-container {flex-grow: 1;overflow: auto; /* Scroll only the table container */border: 1px solid var(--border-color);border-radius: var(--border-radius-small);
}#dataPointTable {width: 100%;border-collapse: collapse;font-size: 0.85rem;
}#dataPointTable th,
#dataPointTable td {padding: 0.6rem 0.75rem;text-align: left;border-bottom: 1px solid var(--border-color);white-space: nowrap;
}#dataPointTable th {background-color: var(--table-header-bg);font-weight: 500;color: var(--text-label);position: sticky;top: 0;z-index: 1;
}#dataPointTable tbody tr:hover {background-color: var(--table-row-hover-bg);
}#dataPointTable td:nth-child(1) { /* Tag Name */font-weight: 500;font-family: var(--font-monospace);color: var(--text-primary);
}#dataPointTable td:nth-child(2) { /* Value */font-weight: 600;font-family: var(--font-monospace);
}#dataPointTable td:nth-child(3) { /* Source PLC */color: var(--text-secondary);
}#dataPointTable td:nth-child(4) { /* Timestamp */font-size: 0.8rem;color: var(--text-secondary);
}#dataPointTable td:nth-child(5) { /* Status */text-align: center;
}.data-quality-indicator {display: inline-block;width: 10px;height: 10px;border-radius: 50%;margin-right: 0.3em;
}.quality-good .data-quality-indicator { background-color: var(--status-quality-good); }
.quality-uncertain .data-quality-indicator { background-color: var(--status-quality-uncertain); }
.quality-bad .data-quality-indicator { background-color: var(--status-quality-bad); }.quality-good { color: var(--status-quality-good); }
.quality-uncertain { color: var(--status-quality-uncertain); }
.quality-bad { color: var(--status-quality-bad); }.placeholder-row td {text-align: center;color: var(--placeholder-text);font-style: italic;height: 100px; /* Give placeholder some height */
}/* Log Panel */
.log-panel {grid-column: span 1; /* Takes last column space */
}.log-level-filter {display: flex;gap: 0.75rem;margin-left: auto; /* Push filter to the right */font-size: 0.8rem;
}.log-level-filter label {display: flex;align-items: center;gap: 0.2rem;cursor: pointer;color: var(--text-label);
}
.log-level-filter input[type="checkbox"] {margin-right: 0.1rem;
}.log-list {list-style: none;overflow-y: auto;flex-grow: 1;font-family: var(--font-monospace);font-size: 0.8rem;line-height: 1.5;
}.log-item {padding: 0.3rem 0.5rem;border-bottom: 1px dashed #f0f0f0;display: flex;gap: 0.75rem;align-items: flex-start;
}.log-item:last-child {border-bottom: none;
}.log-item .timestamp {color: var(--text-secondary);flex-shrink: 0;width: 60px; /* Fixed width for alignment */
}.log-item .level {font-weight: 600;flex-shrink: 0;width: 45px; /* Fixed width for alignment */text-align: right;padding-right: 0.5rem;
}.log-item.info .level { color: var(--log-info); }
.log-item.warn .level { color: var(--log-warn); }
.log-item.error .level { color: var(--log-error); }.log-item .message {color: var(--text-secondary);flex-grow: 1;word-break: break-word;
}/* Footer */
.main-footer {background-color: var(--header-bg);padding: 0.6rem 1.5rem;text-align: center;font-size: 0.8rem;color: var(--text-secondary);border-top: 1px solid var(--border-color);flex-shrink: 0;
}/* Icons (Basic Placeholders) */
.icon::before {display: inline-block;font-weight: normal;font-style: normal;font-variant: normal;text-rendering: auto;-webkit-font-smoothing: antialiased;margin-right: 0.3em;
}
.icon-connect::before { content: "🔌"; }
.icon-tags::before { content: "🏷️"; }
.icon-log::before { content: "📜"; }
.icon-plus::before { content: "+"; font-weight: bold; } 
.icon-clear::before { content: "🗑️"; }/* Placeholder */
.placeholder {color: var(--placeholder-text);font-style: italic;text-align: center;padding: 1rem;font-size: 0.9rem;
}/* Responsive Design */
@media (max-width: 1400px) {.main-content {grid-template-columns: 280px 1fr 400px; /* Adjust column widths */}
}@media (max-width: 1200px) {.main-content {grid-template-columns: 250px 1fr 350px; /* Further adjust */}.main-header h1 {font-size: 1.15rem;}.log-panel {font-size: 0.75rem; /* Smaller log font */}
}@media (max-width: 992px) {.middleware-container {height: auto;min-height: 100vh;margin: 0.5rem;}.main-content {grid-template-columns: 1fr; /* Stack columns */grid-template-rows: auto auto auto; /* Auto rows */padding: 0.5rem;gap: 0.5rem;}.panel {padding: 0.75rem;overflow-y: visible; /* Allow panels to grow */}.connection-list,.log-list {max-height: 250px; /* Limit list height */overflow-y: auto;}.data-point-table-container {max-height: 350px; /* Limit table height */}.main-header {flex-direction: column;align-items: flex-start;gap: 0.5rem;padding: 0.75rem;}.header-status {align-self: flex-end;}.panel-toolbar {flex-direction: column;align-items: stretch;}.log-level-filter {justify-content: space-around;margin-left: 0;margin-top: 0.5rem;}
}@media (max-width: 576px) {.main-header h1 {font-size: 1rem;}.panel h2 {font-size: 1rem;}#dataPointTable {font-size: 0.75rem; /* Smaller table font on mobile */}#dataPointTable th,#dataPointTable td {padding: 0.4rem 0.5rem;}.connection-list,.log-list {max-height: 200px;}.data-point-table-container {max-height: 300px;}
} 

script.js

// script.js - Industrial Protocol Converter & Data Acquisition Middleware Componentdocument.addEventListener('DOMContentLoaded', () => {// --- DOM Elements ---const globalStatusIndicator = document.getElementById('globalStatusIndicator');const globalStatusText = document.getElementById('globalStatusText');const connectionSearchInput = document.getElementById('connectionSearch');const connectionListUl = document.getElementById('connectionList');const dataPointSearchInput = document.getElementById('dataPointSearch');const dataSourceFilterSelect = document.getElementById('dataSourceFilter');const monitoredPointsCountSpan = document.getElementById('monitoredPointsCount');const dataPointTableBody = document.getElementById('dataPointTBody');const clearLogBtn = document.getElementById('clearLogBtn');const logListUl = document.getElementById('logList');const logLevelFilters = document.querySelectorAll('input[name="logLevel"]');// --- Simulation State & Parameters ---let connections = {};let dataPoints = {}; // Key: tagId, Value: { tagName, value, sourceId, sourceName, timestamp, quality }let logs = [];let connectionSearchTerm = '';let dataPointSearchTerm = '';let dataSourceFilter = 'all';let visibleLogLevels = ['INFO', 'WARN', 'ERROR'];let logCounter = 0;const MAX_LOG_ITEMS = 200;// Update intervals (in ms)const connectionUpdateInterval = 5000; // Check/update connection statusconst dataUpdateInterval = 2000; // Update data point values// --- Sample Data Generation ---function generateSampleConnections(count = 5) {const generatedConnections = {};const brands = ['Siemens', 'Rockwell', 'Mitsubishi', 'Omron', 'Schneider'];const protocols = { // Simplified mapping'Siemens': 'S7','Rockwell': 'EtherNet/IP','Mitsubishi': 'MELSEC','Omron': 'FINS','Schneider': 'Modbus TCP'};const baseIP = "192.168.1.";for (let i = 1; i <= count; i++) {const id = `CONN-${String(i).padStart(3, '0')}`;const brand = brands[Math.floor(Math.random() * brands.length)];const protocol = protocols[brand];const ip = `${baseIP}${10 + i}`;generatedConnections[id] = {id: id,name: `${brand} PLC ${i}`,brand: brand,protocol: protocol,ip: ip,status: 'disconnected', // Initial: disconnected, connecting, connected, errortags: generateSampleTags(id, brand, i, 5 + Math.floor(Math.random() * 10))};}return generatedConnections;}function generateSampleTags(connId, brand, lineNum, count = 10) {const tags = {};const dataTypes = ['DINT', 'REAL', 'BOOL', 'STRING'];const prefixes = {'Siemens': 'DB1.DBW','Rockwell': `Line${lineNum}.Tag`,'Mitsubishi': 'D','Omron': 'D','Schneider': '%MW'};for (let i = 0; i < count; i++) {const dataType = dataTypes[Math.floor(Math.random() * dataTypes.length)];const address = `${prefixes[brand]}${100 + i * 2}`;const tagName = `${connId}.${dataType === 'BOOL' ? 'StatusBit' : 'Sensor'}_${i}`;tags[tagName] = {tagName: tagName,address: address,dataType: dataType,sourceId: connId,sourceName: `${brand} PLC ${lineNum}`// Initial value, quality, timestamp will be added dynamically};}return tags;}// --- Logging Utility ---function addLog(level, message) {const timestamp = new Date().toLocaleTimeString('zh-CN');const logEntry = {id: logCounter++,level: level.toUpperCase(),message: message,timestamp: timestamp};logs.push(logEntry);if (logs.length > MAX_LOG_ITEMS) {logs.shift(); // Remove oldest log}// Only render if the level is visibleif (visibleLogLevels.includes(logEntry.level)) {renderSingleLog(logEntry, true); // Add to top}}// --- Initialization ---function initializeMiddleware() {addLog('info', '中间件服务启动中...');connections = generateSampleConnections();setupEventListeners();// Start simulation loopsupdateConnectionStatuses(); // Initial updatesetInterval(updateConnectionStatuses, connectionUpdateInterval);setInterval(updateDataPoints, dataUpdateInterval);renderConnectionList();renderDataSourceFilter();updateGlobalStatus();renderLogList(); // Render initial logsaddLog('info', '中间件初始化完成。');console.log("Middleware Monitor Initialized", connections);}// --- Event Handlers ---function setupEventListeners() {connectionSearchInput.addEventListener('input', handleConnectionSearch);dataPointSearchInput.addEventListener('input', handleDataPointSearch);dataSourceFilterSelect.addEventListener('change', handleDataSourceFilter);clearLogBtn.addEventListener('click', handleClearLogs);logLevelFilters.forEach(input => {input.addEventListener('change', handleLogLevelFilterChange);});}function handleConnectionSearch(event) {connectionSearchTerm = event.target.value.toLowerCase();renderConnectionList();}function handleDataPointSearch(event) {dataPointSearchTerm = event.target.value.toLowerCase();renderDataPointTable();}function handleDataSourceFilter(event) {dataSourceFilter = event.target.value;renderDataPointTable();}function handleClearLogs() {logs = [];logListUl.innerHTML = ''; // Clear displayaddLog('info', '日志已清除。');}function handleLogLevelFilterChange() {visibleLogLevels = Array.from(logLevelFilters).filter(input => input.checked).map(input => input.value);renderLogList(); // Re-render logs based on new filter}// --- Simulation & Data Update Logic ---function updateConnectionStatuses() {let changed = false;Object.values(connections).forEach(conn => {const prevState = conn.status;const rand = Math.random();switch (conn.status) {case 'disconnected':if (rand < 0.3) { // Chance to start connectingconn.status = 'connecting';addLog('info', `尝试连接 ${conn.name} (${conn.ip})...`);}break;case 'connecting':if (rand < 0.7) { // Chance to connect successfullyconn.status = 'connected';addLog('info', `${conn.name} 连接成功.`);initializeDataPointsForConnection(conn);} else if (rand < 0.85) { // Chance to failconn.status = 'error';addLog('error', `${conn.name} 连接失败 (超时或配置错误).`);removeDataPointsForConnection(conn);} // Else: remains connectingbreak;case 'connected':if (rand < 0.03) { // Small chance to disconnectconn.status = 'disconnected';addLog('warn', `${conn.name} 连接已断开.`);removeDataPointsForConnection(conn);} else if (rand < 0.06) { // Small chance of error stateconn.status = 'error';addLog('error', `${conn.name} 连接出现错误 (通信异常).`);// Mark data as uncertain or bad? For now, just show errorsetDataPointsQuality(conn.id, 'uncertain');}break;case 'error':if (rand < 0.2) { // Chance to recover (retry implicitly)conn.status = 'disconnected'; // Go back to disconnected to retryaddLog('info', `${conn.name} 从错误状态恢复,尝试重新连接.`);}break;}if (conn.status !== prevState) {changed = true;}});if (changed) {renderConnectionList();updateGlobalStatus();renderDataPointTable(); // Data points might have been added/removed/quality changed}}function initializeDataPointsForConnection(conn) {Object.values(conn.tags).forEach(tagDef => {if (!dataPoints[tagDef.tagName]) {dataPoints[tagDef.tagName] = {...tagDef,value: generateInitialValue(tagDef.dataType),timestamp: new Date(),quality: 'good'};addLog('info', `${conn.name} 添加数据点: ${tagDef.tagName}`);} else {// If tag already exists but connection was re-establisheddataPoints[tagDef.tagName].quality = 'good';dataPoints[tagDef.tagName].timestamp = new Date();}});updateMonitoredPointsCount();}function removeDataPointsForConnection(conn) {Object.keys(conn.tags).forEach(tagName => {if (dataPoints[tagName]) {// Instead of deleting, mark as bad qualitydataPoints[tagName].quality = 'bad';dataPoints[tagName].value = '#BAD'; // Indicate bad valuedataPoints[tagName].timestamp = new Date();addLog('warn', `数据点 ${tagName} (来自 ${conn.name}) 质量变为 BAD (连接断开).`);// delete dataPoints[tagName]; // Option: remove completely}});// updateMonitoredPointsCount(); // Don't update count if just marking as bad}function setDataPointsQuality(connectionId, quality) {Object.values(dataPoints).forEach(dp => {if (dp.sourceId === connectionId) {dp.quality = quality;dp.timestamp = new Date();if(quality !== 'good') dp.value = `#${quality.toUpperCase()}`;}});}function updateDataPoints() {let dataChanged = false;Object.values(dataPoints).forEach(dp => {const conn = connections[dp.sourceId];if (conn && conn.status === 'connected') {const oldValue = dp.value;const oldQuality = dp.quality;const rand = Math.random();// Simulate value changedp.value = generateNextValue(dp.value, dp.dataType);// Simulate quality changeif (rand < 0.02 && dp.quality === 'good') {dp.quality = 'uncertain';addLog('warn', `数据点 ${dp.tagName} 质量变为 UNCERTAIN.`);} else if (rand < 0.005 && dp.quality === 'good') {dp.quality = 'bad';dp.value = '#BAD';addLog('error', `数据点 ${dp.tagName} 质量变为 BAD (读取错误).`);} else if (dp.quality !== 'good' && rand < 0.3) {// Chance to recover qualitydp.quality = 'good';}if (dp.value !== oldValue || dp.quality !== oldQuality) {dp.timestamp = new Date();dataChanged = true;}}});if (dataChanged) {renderDataPointTable();}}function generateInitialValue(dataType) {switch (dataType) {case 'DINT': return Math.floor(Math.random() * 1000);case 'REAL': return (Math.random() * 100).toFixed(2);case 'BOOL': return Math.random() < 0.5;case 'STRING': return `Status_${Math.random().toString(36).substring(2, 7)}`;default: return null;}}function generateNextValue(currentValue, dataType) {switch (dataType) {case 'DINT':const change = Math.floor((Math.random() - 0.4) * 10);return currentValue + change;case 'REAL':const floatChange = (Math.random() - 0.45) * 5;let newValue = parseFloat(currentValue) + floatChange;return newValue.toFixed(2);case 'BOOL':// Lower chance to flip bool valuereturn Math.random() < 0.05 ? !currentValue : currentValue;case 'STRING':// Less frequent string changesreturn Math.random() < 0.02 ? `Status_${Math.random().toString(36).substring(2, 7)}` : currentValue;default: return currentValue;}}function updateGlobalStatus() {const total = Object.keys(connections).length;const connectedCount = Object.values(connections).filter(c => c.status === 'connected').length;const errorCount = Object.values(connections).filter(c => c.status === 'error').length;const connectingCount = Object.values(connections).filter(c => c.status === 'connecting').length;let statusClass = 'disconnected';let statusText = `已断开 (${total - connectedCount - errorCount - connectingCount}/${total})`;if (errorCount > 0) {statusClass = 'error';statusText = `错误 (${errorCount}/${total})`;} else if (connectingCount > 0) {statusClass = 'connecting';statusText = `连接中 (${connectingCount}/${total})...`;} else if (connectedCount === total && total > 0) {statusClass = 'connected';statusText = `全部已连接 (${connectedCount}/${total})`;} else if (connectedCount > 0) {statusClass = 'connected'; // Show connected even if not all arestatusText = `部分连接 (${connectedCount}/${total})`;} else if (total === 0) {statusText = '无连接';}globalStatusIndicator.className = `status-indicator ${statusClass}`;globalStatusText.textContent = statusText;}function updateMonitoredPointsCount() {monitoredPointsCountSpan.textContent = Object.keys(dataPoints).length;}// --- Rendering Functions ---function renderConnectionList() {connectionListUl.innerHTML = ''; // Clear listconst filteredConnections = Object.values(connections).filter(conn =>conn.name.toLowerCase().includes(connectionSearchTerm)|| conn.ip.toLowerCase().includes(connectionSearchTerm)|| conn.protocol.toLowerCase().includes(connectionSearchTerm));if (filteredConnections.length === 0) {connectionListUl.innerHTML = '<li class="placeholder">未找到匹配连接</li>';return;}filteredConnections.sort((a, b) => a.name.localeCompare(b.name)).forEach(conn => {const li = document.createElement('li');li.className = 'connection-item';li.dataset.id = conn.id;li.innerHTML = `<div class="connection-details"><span class="connection-name">${conn.name} <span class="protocol">${conn.protocol}</span></span><span class="connection-info">${conn.ip}</span></div><span class="connection-status ${conn.status}"><span class="dot"></span>${conn.status.toUpperCase()}</span>`;connectionListUl.appendChild(li);});}function renderDataSourceFilter() {const sources = [...new Set(Object.values(connections).map(c => c.name))].sort();dataSourceFilterSelect.innerHTML = '<option value="all">所有源</option>'; // Resetsources.forEach(sourceName => {const option = document.createElement('option');option.value = sourceName;option.textContent = sourceName;dataSourceFilterSelect.appendChild(option);});}function renderDataPointTable() {dataPointTableBody.innerHTML = ''; // Clear tableconst filteredDataPoints = Object.values(dataPoints).filter(dp => {const searchTermLower = dataPointSearchTerm.toLowerCase();const matchesSearch = searchTermLower === ''|| dp.tagName.toLowerCase().includes(searchTermLower)|| String(dp.value).toLowerCase().includes(searchTermLower);const matchesSource = dataSourceFilter === 'all' || dp.sourceName === dataSourceFilter;return matchesSearch && matchesSource;});if (filteredDataPoints.length === 0) {dataPointTableBody.innerHTML = '<tr class="placeholder-row"><td colspan="5">无匹配数据点</td></tr>';updateMonitoredPointsCount(); // Reflect filtered count? Or total? Let's show total.// monitoredPointsCountSpan.textContent = filteredDataPoints.length; // Option: show filtered countreturn;}filteredDataPoints.sort((a, b) => a.tagName.localeCompare(b.tagName)) // Sort by tag name.forEach(dp => {const tr = document.createElement('tr');tr.dataset.tagId = dp.tagName;const qualityClass = `quality-${dp.quality}`;tr.innerHTML = `<td>${dp.tagName}</td><td>${formatValue(dp.value, dp.dataType)}</td><td>${dp.sourceName}</td><td>${dp.timestamp.toLocaleTimeString('zh-CN', { hour12: false })}.${String(dp.timestamp.getMilliseconds()).padStart(3, '0')}</td><td><span class="${qualityClass}"><span class="data-quality-indicator"></span>${dp.quality.toUpperCase()}</span></td>`;dataPointTableBody.appendChild(tr);});// updateMonitoredPointsCount(); // Update total count in the header}function formatValue(value, dataType) {if (typeof value === 'boolean') {return value ? 'TRUE' : 'FALSE';}if (value === null || value === undefined) {return '--';}// Handle special quality stringsif (typeof value === 'string' && value.startsWith('#')) {return `<span style="color: ${value === '#BAD' ? 'var(--status-quality-bad)' : 'var(--status-quality-uncertain)'}">${value}</span>`;}return String(value);}function renderLogList() {logListUl.innerHTML = ''; // Clear listconst filteredLogs = logs.filter(log => visibleLogLevels.includes(log.level));if (filteredLogs.length === 0) {logListUl.innerHTML = '<li class="log-item placeholder">无匹配日志条目</li>';return;}filteredLogs.forEach(log => renderSingleLog(log, false)); // Add to bottom initiallylogListUl.scrollTop = logListUl.scrollHeight; // Scroll to bottom after initial render}function renderSingleLog(log, prepend = false) {const li = document.createElement('li');li.className = `log-item ${log.level.toLowerCase()}`;li.dataset.logId = log.id;li.innerHTML = `<span class="timestamp">${log.timestamp}</span><span class="level">${log.level}</span><span class="message">${log.message}</span>`;if (prepend) {logListUl.insertBefore(li, logListUl.firstChild);// Optional: Trim logs if prepending frequently to avoid infinite growth in viewif (logListUl.children.length > MAX_LOG_ITEMS * 1.2) { // Keep slightly more than max logs in viewlogListUl.removeChild(logListUl.lastChild);}} else {logListUl.appendChild(li);}}// --- Initial Call ---initializeMiddleware();
}); 

相关文章:

  • 全球宠物经济新周期下的亚马逊跨境采购策略革新——宠物用品赛道成本优化三维路径
  • IP防护等级举例解析
  • 专项智能练习(加强题型)-DA-02
  • websocket入门详解
  • 【Ubuntu】安装BitComet种子下载器
  • 远程实时控制安卓模拟器技术scrcpy
  • 基于EtherCAT与ABP vNext 构建高可用、高性能的工业自动化平台
  • 软考 系统架构设计师系列知识点之杂项集萃(60)
  • Metagloves Pro+Manus Core:一套组合拳打通虚拟制作与现实工业的任督二脉
  • 【笔记】CosyVoice 模型下载小记:简单易懂的两种方法对比
  • Trae 插件 Builder 模式:从 0 到 1 开发天气查询小程序,解锁 AI 编程新体验
  • 康复训练:VR 老年虚拟仿真,趣味助力恢复​
  • IP地址查询可以了解到哪些宿主信息
  • SpringBoot 自动装配流程
  • 培训考试系统在职业技能培训中发挥着怎么样的作用
  • c++作业整理2
  • java中XML的使用
  • 基于EFISH-SCB-RK3576/SAIL-RK3576的智能药柜管理系统技术方案
  • 阿里云的网络有哪些
  • 【药品进销存专用软件】佳易王药品台账管理系统:门诊进销存怎么操作?系统实操教程 #医药系统进销存
  • 金融月评|尽早增强政策力度、调整施策点
  • 悬疑剧背后的女编剧:创作的差异不在性别,而在经验
  • 人民日报整版聚焦:外贸产品拓内销提速增量,多地加快推动内外贸一体化
  • 专家:家长要以身作则,孩子是模仿者学习者有时也是评判者
  • 一图看懂|印巴交火后,双方基地受损多少?
  • “85后”贵阳市政府驻重庆办事处主任吴育材拟任新职