一工业现场痛点与背景在工业现场如自动化生产线、水处理、加料系统中信捷 A-BOX-U/4G/W远程通讯模块常作为连接底层 PLC如信捷 XD/XL 系列、三菱 FX 系列、西门子 S7 系列与上层云平台的通信桥梁。随着 A-BOX 升级到新版 MQTT 协议底层 PLC 寄存器地址被抽象为了动态映射表pub_configlist并引入了毫秒级缓存、批量连续寄存器上报等新特性。在调试过程中电气工程师经常面临以下痛点通信黑盒不知道 A-BOX 是否成功轮询了 PLC 寄存器4G/Wi-Fi 信号抖动时数据有没有丢失。批量点错位信捷新协议中诸如产量[6]的连续寄存器数组、以及下发写控制时产量[2]代表 Index2即第 3 个通道的偏移量陷阱容易导致现场误操作。故障排查慢传统串口调试助手无法直观呈现复杂的 JSON 协议。为此我们在网关层部署了一套运行在5001 端口的 MQTT 实时监控面板。它与5002 端口负责接收自然语言指令并转化为 PLC 写入脚本的“AI 控制下发服务”共同组成了工厂的AIoT 电气综合控制台。二 电气网络与通信拓扑从底层物理设备到云端监控整体电气信号与数据的流向如下。系统采用network_mode: host确保工业网关的高并发与低时延【底层物理电气层】 【边缘网关层 :5001】 【中控/浏览器前端】 ┌─────────────────┐ │ 信捷/西门子 PLC │ └────────┬────────┘ │ RS485 / Modbus RTU (串口总线) ▼ ┌─────────────────┐ │ 信捷 A-BOX 模块 │ └────────┬────────┘ │ 4G / Wi-Fi (MQTT 协议) ▼ ┌─────────────────┐ 订阅 # (全量捕获) ┌──────────────────────┐ │ Mosquitto Broker│ ───────────────────────────►│ Express 监听服务 :5001│ └─────────────────┘ └──────────┬───────────┘ │ SSE (Server-Sent Events) ▼ ┌──────────────────────┐ │ 极客暗色终端 UI 面板 │ │ (提供 DeepSeek AI 诊断)│ └──────────────────────┘️ 后端核心实现含电气数据解析逻辑项目部署于宿主机/opt/mqtt-monitor/目录。后端使用MQTT.js订阅 A-BOX 报文重点在于处理底层设备名、指令 IDOrder_ID以及工业数据类型如INT8S、Float等。1. 后端主程序server.jsconst express require(express); const mqtt require(mqtt); const axios require(axios); const path require(path); const app express(); const PORT process.env.PORT || 5001; const MQTT_BROKER process.env.MQTT_BROKER || mqtt://localhost:1883; const DEEPSEEK_API_KEY process.env.DEEPSEEK_API_KEY || your-key; app.use(express.json()); app.use(express.static(path.join(__dirname, public))); let sseClients []; let mqttClient; // 初始化 MQTT全量捕获现场 A-BOX 报文 function initMqtt() { mqttClient mqtt.connect(MQTT_BROKER); mqttClient.on(connect, () { console.log([电气网关] 已成功连接至 Mosquitto Broker: ${MQTT_BROKER}); mqttClient.subscribe(#, (err) { if (!err) console.log([电气网关] 全量主题订阅成功 (#)); }); }); mqttClient.on(message, (topic, payload) { const messageStr payload.toString(); // 实时流推送至前端电气看板 broadcastSSE({ topic, payload: messageStr, time: new Date().toISOString() }); // 监测主动读指令access_data的回包窗口 checkActiveRequestWindow(topic, messageStr); }); } function broadcastSSE(data) { sseClients.forEach(client client.res.write(data: ${JSON.stringify(data)}\n\n)); } // GET /events - SSE 实时事件流工业现场秒级刷新无常规轮询延迟 app.get(/events, (req, res) { res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); const clientId Date.now(); sseClients.push({ id: clientId, res }); req.on(close, () { sseClients sseClients.filter(c c.id ! clientId); }); }); app.get(/api/status, (req, res) { res.json({ mqttConnected: mqttClient ? mqttClient.connected : false, onlineViewers: sseClients.length }); }); // 20秒主动截获现场 PLC 寄存器数据快照上下文 let activeRequest { isActive: false, timer: null, responseData: null }; // POST /api/request - 强制触发 A-BOX 点名轮询 app.post(/api/request, (req, res) { // 目标 A-BOX 设备的唯一 Topic 前缀 const targetTopic 00910414894BB692612345678/access_data; const payload { Unix: Date.now().toString(), Version: V1.0, Content: alldata // 索取全量寄存器数据含 Localghost 系统状态数据 }; if (mqttClient mqttClient.connected) { mqttClient.publish(targetTopic, JSON.stringify(payload)); activeRequest.isActive true; activeRequest.responseData null; if (activeRequest.timer) clearTimeout(activeRequest.timer); // 开启 20 秒电气响应窗口防止无线链路丢包阻塞 activeRequest.timer setTimeout(() { activeRequest.isActive false; }, 20000); return res.json({ status: requested, message: PLC 点名轮询指令已下发 }); } else { return res.status(500).json({ error: MQTT Broker 未就绪 }); } }); function checkActiveRequestWindow(topic, messageStr) { if (activeRequest.isActive topic.endsWith(/pub_data)) { try { activeRequest.responseData JSON.parse(messageStr); activeRequest.isActive false; if (activeRequest.timer) clearTimeout(activeRequest.timer); } catch (e) { console.error(解析现场数据失败, e); } } } app.get(/api/response, (req, res) { if (activeRequest.responseData) { res.json({ status: success, data: activeRequest.responseData }); activeRequest.responseData null; } else if (activeRequest.isActive) { res.json({ status: waiting }); } else { res.json({ status: timeout, message: PLC 未在窗口期内做出电气响应 }); } }); // POST /api/analyze - 引入 DeepSeek 针对电气参数与报文进行诊断 app.post(/api/analyze, async (req, res) { const { logData } req.body; try { const response await axios.post(https://api.deepseek.com/v1/chat/completions, { model: deepseek-chat, messages: [ { role: system, content: 你是一个精通自动化电气工程、信捷/三菱 PLC、工业现场总线的专家。 请对用户提供的信捷 A-BOX MQTT 报文进行深度诊断。 注意 1. 评估 Unix 毫秒时间戳判断无线网关是否存在 4G/Wi-Fi 丢包或明显的数据高时延工业实时性评估。 2. 观察数据类型如 INT8S, Float, 连续寄存器数组等识别工艺参数是否有异常突变例如温度骤升、产量清零、电压不稳。 3. 检查是否有写值错误状态码如 ERROR0写值失败ERROR1未找到该指令ERROR2其他故障。 请采用专业的电气诊断报告格式Markdown输出条理清晰。 }, { role: user, content: JSON.stringify(logData) } ] }, { headers: { Authorization: Bearer ${DEEPSEEK_API_KEY} } }); res.json({ analysis: response.data.choices[0].message.content }); } catch (error) { res.status(500).json({ error: DeepSeek 连线失败: error.message }); } }); app.listen(PORT, () { console.log([电气控制台] 服务已在端口 ${PORT} 启动...); initMqtt(); });三 极客终端风格 UI 面板 (public/index.html)前端设计采用暗色复古终端风格完美契合工业工控屏、中控室显示器的长期挂机需求护眼且直观。网页预览代码如下:!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title MQTT 实时监控/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: SF Mono, Menlo, Consolas, monospace; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; display: flex; flex-direction: column; height: 100vh; } .header { background: #16213e; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; border-bottom: 2px solid #0f3460; } .header h1 { font-size: 1.2rem; color: #e94560; } .status { font-size: .85rem; display: flex; gap: 16px; } .status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; } .dot.online { background: #2ecc71; box-shadow: 0 0 8px #2ecc71; } .dot.offline { background: #e74c3c; box-shadow: 0 0 8px #e74c3c; } .stats { display: flex; gap: 24px; padding: 12px 24px; background: #16213e80; font-size: .9rem; align-items: center; flex-wrap: wrap; } .stat { display: flex; flex-direction: column; } .stat .label { font-size: .7rem; color: #888; text-transform: uppercase; } .stat .value { font-size: 1.1rem; font-weight: 600; color: #e94560; } .btn-req { background: #238636; color: #fff; border: none; padding: 8px 18px; border-radius: 6px; cursor: pointer; font-size: .85rem; font-weight: 600; margin-left: auto; } .btn-req:hover { background: #2ea043; } .btn-req:disabled { background: #30363d; cursor: not-allowed; } .btn-clear { background: #21262d; color: #8b949e; border: 1px solid #30363d; padding: 6px 16px; border-radius: 6px; cursor: pointer; font-size: .85rem; } .btn-ai { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; border: none; padding: 8px 18px; border-radius: 6px; cursor: pointer; font-size: .85rem; font-weight: 600; } .btn-ai:hover { background: linear-gradient(135deg, #7c93f0, #8b5fbf); } .btn-ai:disabled { background: #30363d; cursor: not-allowed; } #pending-status { color: #d2991d; font-size: .8rem; } #analyze-status { color: #a371f7; font-size: .8rem; } /* 弹窗遮罩 */ .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.75); z-index: 1000; align-items: center; justify-content: center; } .modal-overlay.show { display: flex; } .modal-box { background: #16213e; border: 2px solid #667eea; border-radius: 16px; width: min(720px, 92vw); max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 0 60px rgba(102,126,234,.35); animation: popIn .25s ease; } keyframes popIn { from { opacity: 0; transform: scale(.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } } .modal-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; border-bottom: 1px solid #0f3460; } .modal-header h2 { font-size: 1.05rem; color: #667eea; margin: 0; } .modal-close { background: none; border: none; color: #8b949e; font-size: 1.4rem; cursor: pointer; padding: 4px 8px; border-radius: 6px; line-height: 1; } .modal-close:hover { background: #30363d; color: #e0e0e0; } .modal-body { padding: 20px 24px; overflow-y: auto; flex: 1; font-size: .85rem; line-height: 1.9; color: #e0e0e0; white-space: pre-wrap; } .modal-body .loading { color: #a371f7; text-align: center; padding: 40px; font-size: 1rem; } .modal-body .error { color: #f85149; text-align: center; padding: 20px; } /* 欢迎弹窗 */ .welcome-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.8); z-index: 2000; align-items: center; justify-content: center; } .welcome-overlay.show { display: flex; } .welcome-box { background: linear-gradient(135deg, #0f3460, #16213e); border: 2px solid #e94560; border-radius: 20px; padding: 48px 56px; text-align: center; box-shadow: 0 0 80px rgba(233,69,96,.4); animation: popIn .4s ease; } .welcome-box .wl-emoji { font-size: 3rem; } .welcome-box h2 { font-size: 1.6rem; color: #e94560; margin: 16px 0 0; letter-spacing: 2px; } /* 响应面板 */ .resp-panel { margin: 8px 24px; padding: 12px 16px; background: #16213e; border-radius: 8px; border: 1px solid #0f3460; display: none; } .resp-panel.show { display: block; } .resp-panel h3 { color: #58a6ff; font-size: .85rem; margin-bottom: 8px; } .resp-item { padding: 8px 12px; margin-bottom: 6px; background: #0d1117; border-radius: 6px; font-size: .8rem; } .resp-item .rt { color: #58a6ff; font-size: .7rem; } .resp-item .rp { color: #7ee787; margin-top: 4px; word-break: break-all; white-space: pre-wrap; } #messages { padding: 16px 24px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 4px; } .msg { padding: 10px 16px; border-radius: 8px; background: #16213e; border-left: 3px solid #0f3460; animation: slideIn .2s ease; font-size: .9rem; line-height: 1.6; } keyframes slideIn { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } } .msg.pub_data { border-left-color: #2ecc71; } .msg .topic { color: #e94560; font-weight: 600; font-size: .8rem; display: inline-block; margin-right: 8px; } .msg .time { color: #555; font-size: .7rem; float: right; } .msg .body { word-break: break-all; margin-top: 2px; } .empty { text-align: center; color: #555; padding: 60px; font-size: 1.1rem; } /style /head body !-- 欢迎弹窗 -- div classwelcome-overlay show idwelcome-overlay div classwelcome-box div classwl-emoji/div h2欢迎评委指导br隧道卫士AI项目/h2 /div /div div classheader h1 MQTT 实时监控/h1 div classstatus spanspan classdot idmqtt-dot/span span idmqtt-status连接中/span/span span span idviewer-count0/span 人观看/span /div /div div classstats div classstatspan classlabelBroker/spanspan classvalue idstat-broker-/span/div div classstatspan classlabel消息总数/spanspan classvalue idstat-total0/span/div div classstatspan classlabel运行时间/spanspan classvalue idstat-uptime-/span/div button classbtn-req idbtn-req onclickdoRequest() 主动获取数据/button button classbtn-ai idbtn-ai onclickdoAnalyze() disabled DeepSeek 分析/button span idpending-status/span span idanalyze-status/span button classbtn-clear onclickclearMsgs() 清屏/button /div !-- 响应数据面板 -- div classresp-panel idresp-panel h3 access_data 响应 (pub_data)/h3 div idresp-content/div /div !-- DeepSeek 分析弹窗 -- div classmodal-overlay idanalyze-modal div classmodal-box div classmodal-header h2 DeepSeek 分析报告/h2 button classmodal-close onclickcloseModal()times;/button /div div classmodal-body idanalyze-modal-body/div /div /div div idmessagesdiv classempty⏳ 等待 MQTT 消息.../div/div script let totalMsgs 0; let currentResponseData []; const MAX_MSGS 200; // SSE 连接 const evtSource new EventSource(/events); evtSource.onmessage (event) { totalMsgs; const msg JSON.parse(event.data); addMessage(msg); updateStats(); }; evtSource.onerror () { document.getElementById(mqtt-dot).className dot offline; document.getElementById(mqtt-status).textContent 断开; }; function addMessage(msg) { const container document.getElementById(messages); const empty container.querySelector(.empty); if (empty) empty.remove(); const div document.createElement(div); div.className msg; if (msg.topic.endsWith(/pub_data)) div.classList.add(pub_data); div.innerHTML span classtime${msg.time}/span span classtopic${escapeHtml(msg.topic)}/span div classbody${escapeHtml(msg.payload)}/div ; container.appendChild(div); const atBottom container.scrollHeight - container.scrollTop - container.clientHeight 80; if (atBottom) container.scrollTop container.scrollHeight; while (container.children.length MAX_MSGS) { container.firstElementChild.remove(); } } function updateStats() { document.getElementById(mqtt-dot).className dot online; document.getElementById(mqtt-status).textContent 在线; document.getElementById(stat-total).textContent totalMsgs; } function clearMsgs() { document.getElementById(messages).innerHTML div classempty 已清屏等待新消息.../div; } function escapeHtml(str) { const div document.createElement(div); div.textContent str; return div.innerHTML; } // 主动获取数据 async function doRequest() { const btn document.getElementById(btn-req); const pendingEl document.getElementById(pending-status); btn.disabled true; pendingEl.textContent ⏳ 等待响应...; const panel document.getElementById(resp-panel); const content document.getElementById(resp-content); panel.classList.add(show); content.innerHTML span stylecolor:#d2991d;⏳ 已发送 access_data等待设备回复... (13 秒窗口)/span; await fetch(/api/request, { method: POST }); let resolved false; const checkInterval setInterval(async () { if (resolved) return; const r await fetch(/api/response); const d await r.json(); if (d.data.length 0) { resolved true; clearInterval(checkInterval); showResponse(d.data); btn.disabled false; pendingEl.textContent ✅ 完成; } if (!d.pending) { resolved true; clearInterval(checkInterval); if (d.data.length 0) { content.innerHTML span stylecolor:#f85149;❌ 13 秒内未收到 pub_data 响应/span; } btn.disabled false; pendingEl.textContent ; } }, 500); setTimeout(() { btn.disabled false; }, 15000); } function showResponse(data) { currentResponseData data; document.getElementById(btn-ai).disabled false; const content document.getElementById(resp-content); content.innerHTML ; data.forEach((d) { let parsed; try { parsed JSON.stringify(JSON.parse(d.payload), null, 2); } catch(e) { parsed d.payload; } content.innerHTML div classresp-item div classrt${d.time} — ${d.topic}/div pre classrp${escapeHtml(parsed)}/pre /div; }); } // DeepSeek 分析 async function doAnalyze() { if (currentResponseData.length 0) return; const btn document.getElementById(btn-ai); const statusEl document.getElementById(analyze-status); const modal document.getElementById(analyze-modal); const body document.getElementById(analyze-modal-body); btn.disabled true; statusEl.textContent ⏳ 分析中...; modal.classList.add(show); body.innerHTML div classloading⏳ DeepSeek 正在分析数据.../div; try { const r await fetch(/api/analyze, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ data: currentResponseData }), }); const d await r.json(); if (d.success) { body.innerHTML escapeHtml(d.analysis); statusEl.textContent ✅ 分析完成; } else { body.innerHTML div classerror❌ ${escapeHtml(d.error)}/div; statusEl.textContent ❌ 分析失败; } } catch (err) { body.innerHTML div classerror❌ 请求失败: ${escapeHtml(err.message)}/div; statusEl.textContent ❌ 网络错误; } btn.disabled false; setTimeout(() { statusEl.textContent ; }, 5000); } function closeModal() { document.getElementById(analyze-modal).classList.remove(show); } // 欢迎弹窗 3 秒后自动消失 setTimeout(() { document.getElementById(welcome-overlay).classList.remove(show); }, 3000); // 点击遮罩关闭弹窗 document.addEventListener(click, (e) { if (e.target.id analyze-modal) closeModal(); }); // ESC 关闭弹窗 document.addEventListener(keydown, (e) { if (e.key Escape) closeModal(); }); // 定期刷新状态 setInterval(async () { try { const resp await fetch(/api/status); const s await resp.json(); document.getElementById(stat-broker).textContent s.mqttBroker; document.getElementById(stat-uptime).textContent formatUptime(s.uptime); document.getElementById(viewer-count).textContent s.viewers; document.getElementById(mqtt-dot).className dot (s.mqttConnected ? online : offline); document.getElementById(mqtt-status).textContent s.mqttConnected ? 在线 : 断开; } catch(e) {} }, 3000); function formatUptime(s) { const h Math.floor(s / 3600), m Math.floor((s % 3600) / 60); return h 0 ? ${h}h ${m}m : ${m}m ${s % 60}s; } /script /body /html四宿主机直通部署 (network_mode: host)为了避免 Docker 默认网桥在处理高频工业总线数据时引起的网络地址转换NAT开销与端口阻隔我们直接将容器挂载在宿主机的网络栈上。1.docker-compose.yml现场部署配置在/opt/mqtt-monitor/下配置version: 3.8 services: mqtt-monitor: build: . container_name: mqtt-monitor restart: always network_mode: host # 关键直接监听并绑定宿主机物理 5001 端口 environment: - PORT5001 - MQTT_BROKERmqtt://localhost:1883 # 直连本地 Mosquitto 工业 Broker - DEEPSEEK_API_KEYsk-your_real_key_here2. 电气总线调试一键编译启动cd /opt/mqtt-monitor/ docker compose up -d --build五成品展示1.主动获取数据2.deepseek分析现场数据六 工业场景运行实效验证解决连续寄存器映射混乱当底层 PLC 上报带有[6]长度的批量数据例如产量[6]: [12, 32, 43]或者平台下发控制回复write_reply包含状态码ERROR0写值失败时前端看板高亮捕获工程师不需要对照数据手册即可知道是哪个点位操作越界。边缘智能诊断示例当我们选中一行现场高频跳变数据送入 DeepSeekAI 能够快速输出形如下方的电气报告[DeepSeek 电气专家诊断方案]通信链路评估报文内 Unix 毫秒时间戳与系统时间差值为 180ms4G 链路响应正常无积压阻塞。寄存器数值评估设备1连续寄存器产量[6]的通道 3 数值突然发生从 53 到 15 的突降非线性递增请检查底层 PLC 计数清零复位触发器RST的逻辑是否误触发、或物理接近开关是否存在电气抖动。这套 5001 端口监控系统通过全量捕获 现场主动点名 AI 现场分析极大地缩短了自动化产线的停机时间MTTR是不可多得的电气调试利器。欢迎同行在评论区探讨交流
【电气实战】基于 Node.js+SSE+DeepSeek 的信捷 A-BOX 工业网关 MQTT 实时监控与 AI 电气诊断面板(5001 端口篇)
发布时间:2026/5/23 9:27:30
一工业现场痛点与背景在工业现场如自动化生产线、水处理、加料系统中信捷 A-BOX-U/4G/W远程通讯模块常作为连接底层 PLC如信捷 XD/XL 系列、三菱 FX 系列、西门子 S7 系列与上层云平台的通信桥梁。随着 A-BOX 升级到新版 MQTT 协议底层 PLC 寄存器地址被抽象为了动态映射表pub_configlist并引入了毫秒级缓存、批量连续寄存器上报等新特性。在调试过程中电气工程师经常面临以下痛点通信黑盒不知道 A-BOX 是否成功轮询了 PLC 寄存器4G/Wi-Fi 信号抖动时数据有没有丢失。批量点错位信捷新协议中诸如产量[6]的连续寄存器数组、以及下发写控制时产量[2]代表 Index2即第 3 个通道的偏移量陷阱容易导致现场误操作。故障排查慢传统串口调试助手无法直观呈现复杂的 JSON 协议。为此我们在网关层部署了一套运行在5001 端口的 MQTT 实时监控面板。它与5002 端口负责接收自然语言指令并转化为 PLC 写入脚本的“AI 控制下发服务”共同组成了工厂的AIoT 电气综合控制台。二 电气网络与通信拓扑从底层物理设备到云端监控整体电气信号与数据的流向如下。系统采用network_mode: host确保工业网关的高并发与低时延【底层物理电气层】 【边缘网关层 :5001】 【中控/浏览器前端】 ┌─────────────────┐ │ 信捷/西门子 PLC │ └────────┬────────┘ │ RS485 / Modbus RTU (串口总线) ▼ ┌─────────────────┐ │ 信捷 A-BOX 模块 │ └────────┬────────┘ │ 4G / Wi-Fi (MQTT 协议) ▼ ┌─────────────────┐ 订阅 # (全量捕获) ┌──────────────────────┐ │ Mosquitto Broker│ ───────────────────────────►│ Express 监听服务 :5001│ └─────────────────┘ └──────────┬───────────┘ │ SSE (Server-Sent Events) ▼ ┌──────────────────────┐ │ 极客暗色终端 UI 面板 │ │ (提供 DeepSeek AI 诊断)│ └──────────────────────┘️ 后端核心实现含电气数据解析逻辑项目部署于宿主机/opt/mqtt-monitor/目录。后端使用MQTT.js订阅 A-BOX 报文重点在于处理底层设备名、指令 IDOrder_ID以及工业数据类型如INT8S、Float等。1. 后端主程序server.jsconst express require(express); const mqtt require(mqtt); const axios require(axios); const path require(path); const app express(); const PORT process.env.PORT || 5001; const MQTT_BROKER process.env.MQTT_BROKER || mqtt://localhost:1883; const DEEPSEEK_API_KEY process.env.DEEPSEEK_API_KEY || your-key; app.use(express.json()); app.use(express.static(path.join(__dirname, public))); let sseClients []; let mqttClient; // 初始化 MQTT全量捕获现场 A-BOX 报文 function initMqtt() { mqttClient mqtt.connect(MQTT_BROKER); mqttClient.on(connect, () { console.log([电气网关] 已成功连接至 Mosquitto Broker: ${MQTT_BROKER}); mqttClient.subscribe(#, (err) { if (!err) console.log([电气网关] 全量主题订阅成功 (#)); }); }); mqttClient.on(message, (topic, payload) { const messageStr payload.toString(); // 实时流推送至前端电气看板 broadcastSSE({ topic, payload: messageStr, time: new Date().toISOString() }); // 监测主动读指令access_data的回包窗口 checkActiveRequestWindow(topic, messageStr); }); } function broadcastSSE(data) { sseClients.forEach(client client.res.write(data: ${JSON.stringify(data)}\n\n)); } // GET /events - SSE 实时事件流工业现场秒级刷新无常规轮询延迟 app.get(/events, (req, res) { res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); const clientId Date.now(); sseClients.push({ id: clientId, res }); req.on(close, () { sseClients sseClients.filter(c c.id ! clientId); }); }); app.get(/api/status, (req, res) { res.json({ mqttConnected: mqttClient ? mqttClient.connected : false, onlineViewers: sseClients.length }); }); // 20秒主动截获现场 PLC 寄存器数据快照上下文 let activeRequest { isActive: false, timer: null, responseData: null }; // POST /api/request - 强制触发 A-BOX 点名轮询 app.post(/api/request, (req, res) { // 目标 A-BOX 设备的唯一 Topic 前缀 const targetTopic 00910414894BB692612345678/access_data; const payload { Unix: Date.now().toString(), Version: V1.0, Content: alldata // 索取全量寄存器数据含 Localghost 系统状态数据 }; if (mqttClient mqttClient.connected) { mqttClient.publish(targetTopic, JSON.stringify(payload)); activeRequest.isActive true; activeRequest.responseData null; if (activeRequest.timer) clearTimeout(activeRequest.timer); // 开启 20 秒电气响应窗口防止无线链路丢包阻塞 activeRequest.timer setTimeout(() { activeRequest.isActive false; }, 20000); return res.json({ status: requested, message: PLC 点名轮询指令已下发 }); } else { return res.status(500).json({ error: MQTT Broker 未就绪 }); } }); function checkActiveRequestWindow(topic, messageStr) { if (activeRequest.isActive topic.endsWith(/pub_data)) { try { activeRequest.responseData JSON.parse(messageStr); activeRequest.isActive false; if (activeRequest.timer) clearTimeout(activeRequest.timer); } catch (e) { console.error(解析现场数据失败, e); } } } app.get(/api/response, (req, res) { if (activeRequest.responseData) { res.json({ status: success, data: activeRequest.responseData }); activeRequest.responseData null; } else if (activeRequest.isActive) { res.json({ status: waiting }); } else { res.json({ status: timeout, message: PLC 未在窗口期内做出电气响应 }); } }); // POST /api/analyze - 引入 DeepSeek 针对电气参数与报文进行诊断 app.post(/api/analyze, async (req, res) { const { logData } req.body; try { const response await axios.post(https://api.deepseek.com/v1/chat/completions, { model: deepseek-chat, messages: [ { role: system, content: 你是一个精通自动化电气工程、信捷/三菱 PLC、工业现场总线的专家。 请对用户提供的信捷 A-BOX MQTT 报文进行深度诊断。 注意 1. 评估 Unix 毫秒时间戳判断无线网关是否存在 4G/Wi-Fi 丢包或明显的数据高时延工业实时性评估。 2. 观察数据类型如 INT8S, Float, 连续寄存器数组等识别工艺参数是否有异常突变例如温度骤升、产量清零、电压不稳。 3. 检查是否有写值错误状态码如 ERROR0写值失败ERROR1未找到该指令ERROR2其他故障。 请采用专业的电气诊断报告格式Markdown输出条理清晰。 }, { role: user, content: JSON.stringify(logData) } ] }, { headers: { Authorization: Bearer ${DEEPSEEK_API_KEY} } }); res.json({ analysis: response.data.choices[0].message.content }); } catch (error) { res.status(500).json({ error: DeepSeek 连线失败: error.message }); } }); app.listen(PORT, () { console.log([电气控制台] 服务已在端口 ${PORT} 启动...); initMqtt(); });三 极客终端风格 UI 面板 (public/index.html)前端设计采用暗色复古终端风格完美契合工业工控屏、中控室显示器的长期挂机需求护眼且直观。网页预览代码如下:!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title MQTT 实时监控/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: SF Mono, Menlo, Consolas, monospace; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; display: flex; flex-direction: column; height: 100vh; } .header { background: #16213e; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; border-bottom: 2px solid #0f3460; } .header h1 { font-size: 1.2rem; color: #e94560; } .status { font-size: .85rem; display: flex; gap: 16px; } .status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; } .dot.online { background: #2ecc71; box-shadow: 0 0 8px #2ecc71; } .dot.offline { background: #e74c3c; box-shadow: 0 0 8px #e74c3c; } .stats { display: flex; gap: 24px; padding: 12px 24px; background: #16213e80; font-size: .9rem; align-items: center; flex-wrap: wrap; } .stat { display: flex; flex-direction: column; } .stat .label { font-size: .7rem; color: #888; text-transform: uppercase; } .stat .value { font-size: 1.1rem; font-weight: 600; color: #e94560; } .btn-req { background: #238636; color: #fff; border: none; padding: 8px 18px; border-radius: 6px; cursor: pointer; font-size: .85rem; font-weight: 600; margin-left: auto; } .btn-req:hover { background: #2ea043; } .btn-req:disabled { background: #30363d; cursor: not-allowed; } .btn-clear { background: #21262d; color: #8b949e; border: 1px solid #30363d; padding: 6px 16px; border-radius: 6px; cursor: pointer; font-size: .85rem; } .btn-ai { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; border: none; padding: 8px 18px; border-radius: 6px; cursor: pointer; font-size: .85rem; font-weight: 600; } .btn-ai:hover { background: linear-gradient(135deg, #7c93f0, #8b5fbf); } .btn-ai:disabled { background: #30363d; cursor: not-allowed; } #pending-status { color: #d2991d; font-size: .8rem; } #analyze-status { color: #a371f7; font-size: .8rem; } /* 弹窗遮罩 */ .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.75); z-index: 1000; align-items: center; justify-content: center; } .modal-overlay.show { display: flex; } .modal-box { background: #16213e; border: 2px solid #667eea; border-radius: 16px; width: min(720px, 92vw); max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 0 60px rgba(102,126,234,.35); animation: popIn .25s ease; } keyframes popIn { from { opacity: 0; transform: scale(.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } } .modal-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; border-bottom: 1px solid #0f3460; } .modal-header h2 { font-size: 1.05rem; color: #667eea; margin: 0; } .modal-close { background: none; border: none; color: #8b949e; font-size: 1.4rem; cursor: pointer; padding: 4px 8px; border-radius: 6px; line-height: 1; } .modal-close:hover { background: #30363d; color: #e0e0e0; } .modal-body { padding: 20px 24px; overflow-y: auto; flex: 1; font-size: .85rem; line-height: 1.9; color: #e0e0e0; white-space: pre-wrap; } .modal-body .loading { color: #a371f7; text-align: center; padding: 40px; font-size: 1rem; } .modal-body .error { color: #f85149; text-align: center; padding: 20px; } /* 欢迎弹窗 */ .welcome-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.8); z-index: 2000; align-items: center; justify-content: center; } .welcome-overlay.show { display: flex; } .welcome-box { background: linear-gradient(135deg, #0f3460, #16213e); border: 2px solid #e94560; border-radius: 20px; padding: 48px 56px; text-align: center; box-shadow: 0 0 80px rgba(233,69,96,.4); animation: popIn .4s ease; } .welcome-box .wl-emoji { font-size: 3rem; } .welcome-box h2 { font-size: 1.6rem; color: #e94560; margin: 16px 0 0; letter-spacing: 2px; } /* 响应面板 */ .resp-panel { margin: 8px 24px; padding: 12px 16px; background: #16213e; border-radius: 8px; border: 1px solid #0f3460; display: none; } .resp-panel.show { display: block; } .resp-panel h3 { color: #58a6ff; font-size: .85rem; margin-bottom: 8px; } .resp-item { padding: 8px 12px; margin-bottom: 6px; background: #0d1117; border-radius: 6px; font-size: .8rem; } .resp-item .rt { color: #58a6ff; font-size: .7rem; } .resp-item .rp { color: #7ee787; margin-top: 4px; word-break: break-all; white-space: pre-wrap; } #messages { padding: 16px 24px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 4px; } .msg { padding: 10px 16px; border-radius: 8px; background: #16213e; border-left: 3px solid #0f3460; animation: slideIn .2s ease; font-size: .9rem; line-height: 1.6; } keyframes slideIn { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } } .msg.pub_data { border-left-color: #2ecc71; } .msg .topic { color: #e94560; font-weight: 600; font-size: .8rem; display: inline-block; margin-right: 8px; } .msg .time { color: #555; font-size: .7rem; float: right; } .msg .body { word-break: break-all; margin-top: 2px; } .empty { text-align: center; color: #555; padding: 60px; font-size: 1.1rem; } /style /head body !-- 欢迎弹窗 -- div classwelcome-overlay show idwelcome-overlay div classwelcome-box div classwl-emoji/div h2欢迎评委指导br隧道卫士AI项目/h2 /div /div div classheader h1 MQTT 实时监控/h1 div classstatus spanspan classdot idmqtt-dot/span span idmqtt-status连接中/span/span span span idviewer-count0/span 人观看/span /div /div div classstats div classstatspan classlabelBroker/spanspan classvalue idstat-broker-/span/div div classstatspan classlabel消息总数/spanspan classvalue idstat-total0/span/div div classstatspan classlabel运行时间/spanspan classvalue idstat-uptime-/span/div button classbtn-req idbtn-req onclickdoRequest() 主动获取数据/button button classbtn-ai idbtn-ai onclickdoAnalyze() disabled DeepSeek 分析/button span idpending-status/span span idanalyze-status/span button classbtn-clear onclickclearMsgs() 清屏/button /div !-- 响应数据面板 -- div classresp-panel idresp-panel h3 access_data 响应 (pub_data)/h3 div idresp-content/div /div !-- DeepSeek 分析弹窗 -- div classmodal-overlay idanalyze-modal div classmodal-box div classmodal-header h2 DeepSeek 分析报告/h2 button classmodal-close onclickcloseModal()times;/button /div div classmodal-body idanalyze-modal-body/div /div /div div idmessagesdiv classempty⏳ 等待 MQTT 消息.../div/div script let totalMsgs 0; let currentResponseData []; const MAX_MSGS 200; // SSE 连接 const evtSource new EventSource(/events); evtSource.onmessage (event) { totalMsgs; const msg JSON.parse(event.data); addMessage(msg); updateStats(); }; evtSource.onerror () { document.getElementById(mqtt-dot).className dot offline; document.getElementById(mqtt-status).textContent 断开; }; function addMessage(msg) { const container document.getElementById(messages); const empty container.querySelector(.empty); if (empty) empty.remove(); const div document.createElement(div); div.className msg; if (msg.topic.endsWith(/pub_data)) div.classList.add(pub_data); div.innerHTML span classtime${msg.time}/span span classtopic${escapeHtml(msg.topic)}/span div classbody${escapeHtml(msg.payload)}/div ; container.appendChild(div); const atBottom container.scrollHeight - container.scrollTop - container.clientHeight 80; if (atBottom) container.scrollTop container.scrollHeight; while (container.children.length MAX_MSGS) { container.firstElementChild.remove(); } } function updateStats() { document.getElementById(mqtt-dot).className dot online; document.getElementById(mqtt-status).textContent 在线; document.getElementById(stat-total).textContent totalMsgs; } function clearMsgs() { document.getElementById(messages).innerHTML div classempty 已清屏等待新消息.../div; } function escapeHtml(str) { const div document.createElement(div); div.textContent str; return div.innerHTML; } // 主动获取数据 async function doRequest() { const btn document.getElementById(btn-req); const pendingEl document.getElementById(pending-status); btn.disabled true; pendingEl.textContent ⏳ 等待响应...; const panel document.getElementById(resp-panel); const content document.getElementById(resp-content); panel.classList.add(show); content.innerHTML span stylecolor:#d2991d;⏳ 已发送 access_data等待设备回复... (13 秒窗口)/span; await fetch(/api/request, { method: POST }); let resolved false; const checkInterval setInterval(async () { if (resolved) return; const r await fetch(/api/response); const d await r.json(); if (d.data.length 0) { resolved true; clearInterval(checkInterval); showResponse(d.data); btn.disabled false; pendingEl.textContent ✅ 完成; } if (!d.pending) { resolved true; clearInterval(checkInterval); if (d.data.length 0) { content.innerHTML span stylecolor:#f85149;❌ 13 秒内未收到 pub_data 响应/span; } btn.disabled false; pendingEl.textContent ; } }, 500); setTimeout(() { btn.disabled false; }, 15000); } function showResponse(data) { currentResponseData data; document.getElementById(btn-ai).disabled false; const content document.getElementById(resp-content); content.innerHTML ; data.forEach((d) { let parsed; try { parsed JSON.stringify(JSON.parse(d.payload), null, 2); } catch(e) { parsed d.payload; } content.innerHTML div classresp-item div classrt${d.time} — ${d.topic}/div pre classrp${escapeHtml(parsed)}/pre /div; }); } // DeepSeek 分析 async function doAnalyze() { if (currentResponseData.length 0) return; const btn document.getElementById(btn-ai); const statusEl document.getElementById(analyze-status); const modal document.getElementById(analyze-modal); const body document.getElementById(analyze-modal-body); btn.disabled true; statusEl.textContent ⏳ 分析中...; modal.classList.add(show); body.innerHTML div classloading⏳ DeepSeek 正在分析数据.../div; try { const r await fetch(/api/analyze, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ data: currentResponseData }), }); const d await r.json(); if (d.success) { body.innerHTML escapeHtml(d.analysis); statusEl.textContent ✅ 分析完成; } else { body.innerHTML div classerror❌ ${escapeHtml(d.error)}/div; statusEl.textContent ❌ 分析失败; } } catch (err) { body.innerHTML div classerror❌ 请求失败: ${escapeHtml(err.message)}/div; statusEl.textContent ❌ 网络错误; } btn.disabled false; setTimeout(() { statusEl.textContent ; }, 5000); } function closeModal() { document.getElementById(analyze-modal).classList.remove(show); } // 欢迎弹窗 3 秒后自动消失 setTimeout(() { document.getElementById(welcome-overlay).classList.remove(show); }, 3000); // 点击遮罩关闭弹窗 document.addEventListener(click, (e) { if (e.target.id analyze-modal) closeModal(); }); // ESC 关闭弹窗 document.addEventListener(keydown, (e) { if (e.key Escape) closeModal(); }); // 定期刷新状态 setInterval(async () { try { const resp await fetch(/api/status); const s await resp.json(); document.getElementById(stat-broker).textContent s.mqttBroker; document.getElementById(stat-uptime).textContent formatUptime(s.uptime); document.getElementById(viewer-count).textContent s.viewers; document.getElementById(mqtt-dot).className dot (s.mqttConnected ? online : offline); document.getElementById(mqtt-status).textContent s.mqttConnected ? 在线 : 断开; } catch(e) {} }, 3000); function formatUptime(s) { const h Math.floor(s / 3600), m Math.floor((s % 3600) / 60); return h 0 ? ${h}h ${m}m : ${m}m ${s % 60}s; } /script /body /html四宿主机直通部署 (network_mode: host)为了避免 Docker 默认网桥在处理高频工业总线数据时引起的网络地址转换NAT开销与端口阻隔我们直接将容器挂载在宿主机的网络栈上。1.docker-compose.yml现场部署配置在/opt/mqtt-monitor/下配置version: 3.8 services: mqtt-monitor: build: . container_name: mqtt-monitor restart: always network_mode: host # 关键直接监听并绑定宿主机物理 5001 端口 environment: - PORT5001 - MQTT_BROKERmqtt://localhost:1883 # 直连本地 Mosquitto 工业 Broker - DEEPSEEK_API_KEYsk-your_real_key_here2. 电气总线调试一键编译启动cd /opt/mqtt-monitor/ docker compose up -d --build五成品展示1.主动获取数据2.deepseek分析现场数据六 工业场景运行实效验证解决连续寄存器映射混乱当底层 PLC 上报带有[6]长度的批量数据例如产量[6]: [12, 32, 43]或者平台下发控制回复write_reply包含状态码ERROR0写值失败时前端看板高亮捕获工程师不需要对照数据手册即可知道是哪个点位操作越界。边缘智能诊断示例当我们选中一行现场高频跳变数据送入 DeepSeekAI 能够快速输出形如下方的电气报告[DeepSeek 电气专家诊断方案]通信链路评估报文内 Unix 毫秒时间戳与系统时间差值为 180ms4G 链路响应正常无积压阻塞。寄存器数值评估设备1连续寄存器产量[6]的通道 3 数值突然发生从 53 到 15 的突降非线性递增请检查底层 PLC 计数清零复位触发器RST的逻辑是否误触发、或物理接近开关是否存在电气抖动。这套 5001 端口监控系统通过全量捕获 现场主动点名 AI 现场分析极大地缩短了自动化产线的停机时间MTTR是不可多得的电气调试利器。欢迎同行在评论区探讨交流