This commit is contained in:
张成
2026-04-10 18:45:10 +08:00
parent 37daa2f99f
commit 7ef0c68ad1
4 changed files with 139 additions and 24 deletions

View File

@@ -8,15 +8,45 @@ class MqttSyncClient {
constructor(brokerUrl, options = {}) { constructor(brokerUrl, options = {}) {
this.client = mqtt.connect(brokerUrl, options) this.client = mqtt.connect(brokerUrl, options)
this.isConnected = false this.isConnected = false
/** @type {string[]} 需在每次 connect含重连后向 Broker 幂等订阅的主题 */
this._maintainedTopics = []
/** 最近一次收到任意 `response` 主题消息的时间(用于超时日志关联) */
this.lastResponseAt = null
// 使用 Map 结构优化消息监听器,按 topic 分组 // 使用 Map 结构优化消息监听器,按 topic 分组
this.messageListeners = new Map(); // Map<topic, Set<listener>> this.messageListeners = new Map(); // Map<topic, Set<listener>>
this.globalListeners = new Set(); // 全局监听器(监听所有 topic this.globalListeners = new Set(); // 全局监听器(监听所有 topic
const ts = () => new Date().toISOString()
const markDisconnected = (reason) => {
this.isConnected = false
console.warn(`[MQTT] ${ts()} 连接不可用 reason=${reason}`)
}
this.client.on('connect', () => { this.client.on('connect', () => {
this.isConnected = true this.isConnected = true
console.log(`[MQTT] ${ts()} 服务端已连接(含重连后的 connect`)
this._resubscribeMaintainedTopics()
})
console.log('MQTT 服务端已连接') this.client.on('reconnect', () => {
console.log(`[MQTT] ${ts()} 正在重连 Broker...`)
})
this.client.on('offline', () => {
markDisconnected('offline')
})
this.client.on('disconnect', () => {
markDisconnected('disconnect')
})
this.client.on('close', () => {
markDisconnected('close')
})
this.client.on('end', () => {
markDisconnected('end')
}) })
this.client.on('message', (topic, message) => { this.client.on('message', (topic, message) => {
@@ -29,6 +59,9 @@ class MqttSyncClient {
return; return;
} }
if (topic === 'response') {
this.lastResponseAt = Date.now()
}
// 1. 触发该 topic 的专用监听器 // 1. 触发该 topic 的专用监听器
const topicListeners = this.messageListeners.get(topic); const topicListeners = this.messageListeners.get(topic);
@@ -56,18 +89,52 @@ class MqttSyncClient {
}) })
this.client.on('error', (err) => { this.client.on('error', (err) => {
console.warn('[MQTT] Error:', err.message) console.warn(`[MQTT] ${ts()} Error:`, err && err.message ? err.message : err)
}) })
} }
/**
* 与 mqtt.js 原生 connected 对齐,供单例健康检查
*/
isBrokerConnected() {
return !!(this.client && this.client.connected)
}
/**
* 注册需在每次 connect 后向 Broker 重新声明订阅的主题(不重复注册消息监听器)
* @param {string[]} topics
*/
setMaintainedTopics(topics) {
this._maintainedTopics = Array.isArray(topics) ? [...topics] : []
}
_resubscribeMaintainedTopics() {
if (!this._maintainedTopics.length) return
if (!this.client || !this.client.connected) return
const ts = new Date().toISOString()
for (const topic of this._maintainedTopics) {
this.client.subscribe(topic, { qos: 0 }, (err, granted) => {
if (err) {
console.warn(`[MQTT] ${ts} ensureSubscriptions 订阅失败 topic=${topic}`, err.message || err)
} else {
console.log(`[MQTT] ${ts} ensureSubscriptions 已订阅 topic=${topic}`, granted)
}
})
}
}
waitForConnect(timeout = 5000) { waitForConnect(timeout = 5000) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.isConnected) return resolve() if (this.isBrokerConnected()) {
this.isConnected = true
return resolve()
}
const timer = setTimeout(() => { const timer = setTimeout(() => {
reject(new Error('MQTT connect timeout')) reject(new Error('MQTT connect timeout'))
}, timeout) }, timeout)
const check = () => { const check = () => {
if (this.isConnected) { if (this.isBrokerConnected()) {
this.isConnected = true
clearTimeout(timer) clearTimeout(timer)
resolve() resolve()
} else { } else {
@@ -113,7 +180,6 @@ class MqttSyncClient {
resolve(granted) resolve(granted)
} }
} }
1
}) })
}) })
} }
@@ -143,7 +209,12 @@ class MqttSyncClient {
const timer = setTimeout(() => { const timer = setTimeout(() => {
this.removeMessageListener(onMessage); this.removeMessageListener(onMessage);
reject(new Error('Timeout waiting for response')); const last = this.lastResponseAt
const extra = last
? ` lastResponseAt=${new Date(last).toISOString()} brokerConnected=${this.isBrokerConnected()}`
: ` brokerConnected=${this.isBrokerConnected()}`
console.warn(`[MQTT] ${new Date().toISOString()} publishAndWait 超时 uuid=${uuid} topic=request_${sn_code}${extra}`)
reject(new Error('Timeout waiting for response' + (last ? `; lastResponseAt=${new Date(last).toISOString()}` : '')));
}, timeout); }, timeout);
const onMessage = (topic, message) => { const onMessage = (topic, message) => {
@@ -242,6 +313,7 @@ class MqttSyncClient {
} }
end(force = false) { end(force = false) {
this.isConnected = false
this.client.end(force) this.client.end(force)
} }
} }

View File

@@ -1,8 +1,30 @@
const MqttSyncClient = require('./mqttClient'); const MqttSyncClient = require('./mqttClient');
const Framework = require('../../../framework/node-core-framework'); const Framework = require('../../../framework/node-core-framework');
const logs = require('../logProxy'); const logs = require('../logProxy');
const appConfig = require('../../../config/config.js');
// action.js 已合并到 mqttDispatcher.js不再需要单独引入 // action.js 已合并到 mqttDispatcher.js不再需要单独引入
function buildMqttManagerConfig() {
const mqttCfg = appConfig.mqtt || {};
const brokerUrl = (mqttCfg.brokerUrl && String(mqttCfg.brokerUrl).trim())
? mqttCfg.brokerUrl.trim()
: `mqtt://${mqttCfg.host || '192.144.167.231'}:${mqttCfg.port != null ? mqttCfg.port : 1883}`;
const options = {
clientId: mqttCfg.clientId || `mqtt_server_${Math.random().toString(16).substr(2, 8)}`,
clean: mqttCfg.clean !== false,
connectTimeout: mqttCfg.connectTimeout != null ? mqttCfg.connectTimeout : 5000,
reconnectPeriod: mqttCfg.reconnectPeriod != null ? mqttCfg.reconnectPeriod : 5000,
keepalive: mqttCfg.keepalive != null ? mqttCfg.keepalive : 60
};
if (mqttCfg.username) {
options.username = mqttCfg.username;
}
if (mqttCfg.password) {
options.password = mqttCfg.password;
}
return { brokerUrl, options };
}
/** /**
* MQTT管理器 - 单例模式 * MQTT管理器 - 单例模式
* 负责管理MQTT连接确保全局只有一个MQTT客户端实例 * 负责管理MQTT连接确保全局只有一个MQTT客户端实例
@@ -11,16 +33,7 @@ class MqttManager {
constructor() { constructor() {
this.client = null; this.client = null;
this.isInitialized = false; this.isInitialized = false;
this.config = { this.config = buildMqttManagerConfig();
brokerUrl: 'mqtt://192.144.167.231:1883', // MQTT Broker地址
options: {
clientId: `mqtt_server_${Math.random().toString(16).substr(2, 8)}`,
clean: true,
connectTimeout: 5000,
reconnectPeriod: 5000, // 自动重连间隔
keepalive: 10
}
};
} }
/** /**
@@ -30,8 +43,16 @@ class MqttManager {
*/ */
async getInstance(config = {}) { async getInstance(config = {}) {
if (this.client && this.isInitialized) { if (this.client && this.isInitialized) {
console.log('[MQTT管理器] 返回已存在的MQTT客户端实例'); const brokerOk = typeof this.client.isBrokerConnected === 'function'
return this.client; ? this.client.isBrokerConnected()
: this.client.isConnected;
if (!brokerOk) {
console.warn('[MQTT管理器] 单例已初始化但 Broker 未连接,重置并重建');
await this.reset();
} else {
console.log('[MQTT管理器] 返回已存在的MQTT客户端实例');
return this.client;
}
} }
// 合并配置 // 合并配置
@@ -91,7 +112,13 @@ class MqttManager {
* @returns {boolean} * @returns {boolean}
*/ */
isReady() { isReady() {
return this.isInitialized && this.client && this.client.isConnected; if (!this.isInitialized || !this.client) {
return false;
}
if (typeof this.client.isBrokerConnected === 'function') {
return this.client.isBrokerConnected();
}
return !!this.client.isConnected;
} }
/** /**

View File

@@ -141,6 +141,11 @@ class ScheduleManager {
console.error('[调度管理器] 处理 Boss 消息失败:', error); console.error('[调度管理器] 处理 Boss 消息失败:', error);
} }
}); });
// 重连后向 Broker 幂等声明订阅(监听器仅在上方注册一次,不重复添加)
if (typeof this.mqttClient.setMaintainedTopics === 'function') {
this.mqttClient.setMaintainedTopics(['heartbeat', 'response', 'boss/message']);
}
} }
@@ -152,7 +157,11 @@ class ScheduleManager {
const status = this.mqttDispatcher ? this.mqttDispatcher.getSystemStatus() : {}; const status = this.mqttDispatcher ? this.mqttDispatcher.getSystemStatus() : {};
return { return {
isInitialized: this.isInitialized, isInitialized: this.isInitialized,
mqttConnected: this.mqttClient && this.mqttClient.isConnected, mqttConnected: this.mqttClient && (
typeof this.mqttClient.isBrokerConnected === 'function'
? this.mqttClient.isBrokerConnected()
: this.mqttClient.isConnected
),
systemStats: deviceManager.getSystemStats(), systemStats: deviceManager.getSystemStats(),
allDevices: deviceManager.getAllDevicesStatus(), allDevices: deviceManager.getAllDevicesStatus(),
taskQueues: TaskQueue.getAllDeviceStatus(), taskQueues: TaskQueue.getAllDeviceStatus(),

View File

@@ -71,13 +71,20 @@ module.exports = {
"model": "qwen-plus" "model": "qwen-plus"
}, },
// MQTT配置 // MQTT配置Broker 地址、保活与重连与 Broker 策略对齐时可改此处或环境变量)
mqtt: { mqtt: {
host: process.env.MQTT_HOST || 'localhost', /** 完整连接串,优先于 host+port */
port: process.env.MQTT_PORT || 1883, brokerUrl: process.env.MQTT_BROKER_URL || '',
host: process.env.MQTT_HOST || '192.144.167.231',
port: Number(process.env.MQTT_PORT || 1883),
username: process.env.MQTT_USERNAME || '', username: process.env.MQTT_USERNAME || '',
password: process.env.MQTT_PASSWORD || '', password: process.env.MQTT_PASSWORD || '',
clientId: 'autowork-' + Math.random().toString(16).substr(2, 8) clientId: process.env.MQTT_CLIENT_ID || `mqtt_server_${Math.random().toString(16).substr(2, 8)}`,
clean: true,
connectTimeout: Number(process.env.MQTT_CONNECT_TIMEOUT || 5000),
reconnectPeriod: Number(process.env.MQTT_RECONNECT_PERIOD || 5000),
/** 秒;过小易被 Broker 策略影响,过大对断线感知慢 */
keepalive: Number(process.env.MQTT_KEEPALIVE || 60)
}, },
// 定时任务配置 // 定时任务配置