在上一篇博客中,我们实现了 Vue3 前端页面与硬件设备的 MQTT 通讯交互。但实际开发中,硬件设备往往需要等待生产或调试,此时用 Node.js 模拟硬件设备 就成了高效开发的关键 —— 它能模拟硬件的 MQTT 连接、消息接收、指令响应等核心行为,让前端开发无需依赖真实硬件即可完成联调。
本文将详细解析这份 Node.js 硬件模拟代码,从配置设计、核心功能到通讯逻辑,帮你理解 “模拟硬件” 如何与前端页面实现完整交互,并提供优化建议和实战技巧。
一、模拟硬件的核心目标
在 IoT 通讯场景中,硬件设备的核心行为包括:
- 与 MQTT 服务器建立稳定连接;
- 订阅前端发送的指令主题(如 “搜索硬件”“匹配硬件” 指令);
- 按约定协议响应前端指令(如返回硬件 MAC 地址、确认匹配成功);
- 支持手动发送测试数据,方便调试;
- 处理连接异常、主动断开等边界场景。
这份 Node.js 代码完美覆盖了以上目标,是前端联调、协议验证的理想工具。
二、代码结构与核心模块拆解
整体代码分为 MQTT 配置引入、工具函数、通讯逻辑、命令行交互 四部分,结构清晰且职责分明。我们按模块逐一解析:
模块 1:依赖引入与基础配置
首先引入必要的依赖(MQTT 客户端库、命令行交互库),并定义硬件的核心配置(如 MQTT 连接地址、订阅 / 发布主题、硬件唯一标识 MAC 地址)。
// 1. 引入 MQTT 配置(抽离为单独文件,便于复用和修改)
const { mqtt, connectUrl, connectOptions } = require('./mqttConfig.js');
// 2. 引入 Node.js 命令行交互模块(用于手动输入测试数据)
const readline = require('readline');
// 3. 创建命令行交互实例(实现“输入-发送-提示”的交互流程)
const rl = readline.createInterface({
input: process.stdin, // 输入流(键盘)
output: process.stdout, // 输出流(控制台)
prompt: '请输入要发送的数据 (按回车发送,输入"exit"退出): '
});
// 4. 生成硬件唯一标识(随机 MAC 地址,模拟真实硬件的物理地址)
const mac = generateRandomMac();
// 5. 定义 MQTT 主题(与前端页面约定的通讯“地址”,分两类主题)
// ① 客户级别主题:前端→硬件的广播指令(如搜索硬件)、硬件→前端的广播响应
const sendTopic1 = '/topic2/1111111'; // 硬件→前端(客户级响应)
const receiveTopic1 = '/topic1/1111111';// 前端→硬件(客户级指令)
// ② 点对点主题:前端→指定硬件的指令(如匹配当前硬件)、硬件→前端的点对点响应
const sendTopic2 = `/topic2/${mac}`; // 硬件→前端(点对点响应)
const receiveTopic2 = `/topic1/${mac}`; // 前端→硬件(点对点指令)
// 6. 建立 MQTT 连接(使用配置文件中的连接地址和参数)
const client = mqtt.connect(connectUrl, connectOptions);
关键设计:
- 配置抽离:将 MQTT 服务器地址、端口、客户端 ID 等配置抽离到
mqttConfig.js,避免硬编码,便于不同环境(开发 / 测试)切换; - 双主题设计:
- 客户级别主题(
/topic1/1111111//topic2/1111111):用于前端 “广播” 指令(如搜索所有硬件),所有同客户的硬件都会接收; - 点对点主题(
/topic1/${mac}//topic2/${mac}):用于前端与指定硬件的 “一对一” 通讯(如匹配某个硬件),只有对应 MAC 地址的硬件会响应;
- 客户级别主题(
- 随机 MAC 地址:模拟真实硬件的唯一标识,每次启动模拟设备都会生成新的 MAC,可同时启动多个模拟设备测试 “多硬件搜索”。
模块 2:工具函数实现
核心工具函数 generateRandomMac 用于生成符合格式的随机 MAC 地址,模拟真实硬件的物理标识。
// 生成随机 MAC 地址(格式:XX:XX:XX:XX:XX:XX,XX 为 16 进制字符)
function generateRandomMac() {
// MAC 地址的有效字符(16 进制:0-9、a-f,此处包含大写是为了模拟不同硬件的格式差异)
const hexChars = '0123456789abcdefABCDEF';
let mac = '';
// 生成 6 组 2 位字符(MAC 地址共 6 段)
for (let i = 0; i < 6; i++) {
// 随机取两个 16 进制字符
const char1 = hexChars[Math.floor(Math.random() * 16)];
const char2 = hexChars[Math.floor(Math.random() * 16)];
mac += char1 + char2;
// 除最后一段外,每段后加分隔符“:”
if (i < 5) {
mac += ':';
}
}
return mac;
}
细节说明:
- 真实硬件的 MAC 地址由厂商分配,格式为 6 段 16 进制字符(如
00:1A:2B:3C:4D:5E); - 此处通过
Math.random()随机生成,确保每次启动模拟设备时标识唯一,支持多设备并发测试; - 字符集包含大小写,模拟不同硬件厂商的格式差异,增强模拟的真实性。
模块 3:MQTT 消息发送封装
封装 sendAsText 函数,统一处理消息的 JSON 序列化、发布逻辑和错误提示,避免重复代码。
// 封装 MQTT 消息发送函数(将数据转为 JSON 字符串,统一发布逻辑)
const sendAsText = (topic, text) => {
// 1. 将数据序列化为 JSON 字符串(MQTT 消息需为字符串或 Buffer 格式)
const message = JSON.stringify(text);
// 2. 发布消息到指定主题
client.publish(
topic, // 目标主题
message, // 消息内容
{ qos: 0, retain: false }, // 消息参数(QoS 0:最多一次;retain:不保留消息)
(err) => { // 发布结果回调
if (err) {
console.error('消息发布失败:', err);
} else {
console.log(`\n已发送消息++++++++++++++++++++++++++++++++`);
console.log(`主题: ${topic}`);
console.log(`内容: ${message}`);
}
}
);
};
关键参数说明:
- QoS(Quality of Service):消息服务质量,此处设为 0(最多一次),适合实时性要求不高的场景(如硬件搜索、匹配);若需确保消息必达,可设为 1(至少一次);
- retain:是否保留消息,设为
false表示消息仅推送给当前在线的订阅者,避免新连接的设备收到历史消息; - JSON 序列化:前端与硬件通讯通常使用 JSON 格式(结构化数据,便于解析),此处统一序列化,避免格式不一致问题。
模块 4:MQTT 核心通讯逻辑
这是模拟硬件的核心,包含 连接成功后的订阅、接收前端指令后的响应、连接异常处理 三部分,完全模拟真实硬件的通讯行为。
4.1 连接成功:订阅前端指令主题
当硬件(模拟设备)与 MQTT 服务器连接成功后,立即订阅前端发送指令的两个主题(客户级、点对点),确保能接收前端的所有指令。
// 监听 MQTT 连接成功事件
client.on('connect', () => {
console.log(`硬件 ${mac} 已连接到 MQTT 服务器`);
// 1. 订阅“客户级别”指令主题(接收前端的广播指令,如搜索硬件)
client.subscribe(receiveTopic1, (err) => {
if (err) {
console.error('订阅客户级别主题失败:', err);
} else {
console.log(`已订阅页面发送的客户级别主题: ${receiveTopic1}`);
rl.prompt(); // 显示命令行输入提示
}
});
// 2. 订阅“点对点”指令主题(接收前端针对当前硬件的指令,如匹配硬件)
client.subscribe(receiveTopic2, (err) => {
if (err) {
console.error('订阅点对点主题失败:', err);
} else {
console.log(`\n已订阅页面发送的点对点主题: ${receiveTopic2}`);
rl.prompt(); // 显示命令行输入提示
}
});
});
4.2 接收前端指令:按协议响应
当硬件(模拟设备)收到前端发送的指令后,根据 主题类型 和 指令内容 执行不同逻辑,模拟真实硬件的响应行为(如返回 MAC 地址、确认匹配)。
// 监听 MQTT 消息(接收前端发送的指令)
client.on('message', (topic, message) => {
// 1. 处理“客户级别”指令(前端广播的指令,如搜索硬件)
if (topic === receiveTopic1) {
const textMessage = message.toString(); // 将 Buffer 转为字符串
const parsedMsg = JSON.parse(textMessage); // 解析 JSON 指令
console.log(`\n收到客户级别指令--------------------------`);
console.log(`主题: ${topic}`);
console.log(`内容: ${textMessage}`);
// 模拟“搜索硬件”指令响应(前端发送 status: 'configgateway' 时返回 MAC 地址)
if (parsedMsg.status === 'configgateway') {
// 增加随机概率(模拟部分硬件未响应的场景,更贴近真实环境)
const randomNumber = Math.floor(Math.random() * 100000);
const rander = Math.floor(Math.random() * 10);
if (randomNumber % rander === 0) {
console.log(`\n响应搜索指令:返回硬件 MAC 地址 ${mac}`);
// 向前端发送响应(客户级别主题),携带当前硬件的 MAC 地址
sendAsText(sendTopic1, { config: mac });
} else {
console.log(`\n随机跳过:未响应搜索指令`);
}
}
rl.prompt(); // 重新显示输入提示
}
// 2. 处理“点对点”指令(前端针对当前硬件的指令,如匹配硬件)
if (topic === receiveTopic2) {
const textMessage = message.toString();
const parsedMsg = JSON.parse(textMessage);
console.log(`\n收到点对点指令--------------------------`);
console.log(`主题: ${topic}`);
console.log(`内容: ${textMessage}`);
// 模拟“硬件匹配”指令响应(前端发送 operation: 'pressdown' 时确认匹配)
if (parsedMsg.operation === 'pressdown') {
// 增加随机概率(模拟硬件按键响应的不确定性)
const randomNumber = Math.floor(Math.random() * 100000);
const rander = Math.floor(Math.random() * 10);
if (randomNumber % rander === 0) {
console.log(`\n已按下硬件按键:响应匹配指令`);
// 向前端发送确认(点对点主题),表示匹配成功
sendAsText(sendTopic2, { confirm: 'confirm' });
} else {
console.log(`\n随机跳过:未响应匹配指令`);
}
}
rl.prompt(); // 重新显示输入提示
}
});
关键模拟逻辑:
- 随机响应:通过
randomNumber % rander === 0模拟真实环境中硬件的 “不确定性”(如部分硬件信号弱、未联网,导致无法响应),让测试更贴近实际; - 协议约定:严格按照与前端约定的指令格式响应:
- 前端发送
{ status: 'configgateway' }(搜索硬件)→ 硬件返回{ config: mac }(携带 MAC 地址); - 前端发送
{ operation: 'pressdown' }(匹配硬件)→ 硬件返回{ confirm: 'confirm' }(确认匹配);
- 前端发送
- 主题区分:客户级别指令走广播主题,点对点指令走专属主题,避免指令混淆。
4.3 连接异常处理
处理 MQTT 连接错误、断开等异常场景,确保模拟设备的稳定性,并在控制台给出清晰提示。
// 监听 MQTT 连接错误事件
client.on('error', (err) => {
console.error('\n设备连接错误:', err);
rl.close(); // 关闭命令行交互
});
// 监听 MQTT 连接关闭事件
client.on('close', () => {
console.log('\nMQTT连接已关闭');
});
模块 5:命令行交互逻辑
支持通过命令行手动输入数据并发送到前端,方便调试(如模拟硬件主动上报数据,测试前端接收逻辑)。
// 处理命令行输入(用户手动输入数据并发送)
rl.on('line', (input) => {
const message = input.trim(); // 去除输入的前后空格
// 1. 输入“exit”时,断开 MQTT 连接并退出程序
if (message.toLowerCase() === 'exit') {
console.log('\n正在断开连接...');
client.end(() => { // 断开连接(回调函数确保断开后再退出)
console.log('已断开连接,程序退出');
process.exit(0); // 终止 Node.js 进程
});
return;
}
// 2. 输入非空数据时,发送到客户级别主题(前端可接收)
if (message) {
console.log(`\n手动发送数据:`);
sendAsText(sendTopic1, message); // 调用封装的发送函数
rl.prompt(); // 重新显示输入提示
} else {
// 输入为空时,直接重新提示
rl.prompt();
}
});
三、与 Vue3 前端页面联调步骤
有了这个 Node.js 模拟硬件,我们可以快速与上一篇的 Vue3 前端页面进行联调,步骤如下:
1. 准备工作
- 启动 MQTT 服务器(如 EMQ X、Mosquitto,确保地址和端口与
mqttConfig.js一致); - 确保前端页面的 MQTT 配置(服务器地址、主题格式)与模拟硬件一致(如前端通讯关键字为
1111111,与模拟硬件的receiveTopic1匹配)。
2. 启动模拟硬件
# 1. 安装依赖(若未安装)
npm install mqtt
# 2. 启动模拟设备(可同时启动多个,测试多硬件场景)
node 你的硬件模拟文件名.js
启动后,控制台会显示:
- 硬件 MAC 地址;
- MQTT 连接成功提示;
- 已订阅的主题;
- 命令行输入提示。
3. 前端联调流程
流程 1:搜索硬件
- 在 Vue3 页面点击「匹配硬件」按钮;
- 前端发送
{ status: 'configgateway' }到receiveTopic1(/topic1/1111111); - 模拟硬件收到指令后,随机返回
{ config: mac }到sendTopic1(/topic2/1111111); - 前端接收响应,将 MAC 地址加入硬件列表,完成搜索。
流程 2:匹配硬件
- 前端搜索到硬件列表后,自动逐个发送
{ operation: 'pressdown' }到每个硬件的点对点主题(/topic1/${mac}); - 模拟硬件收到指令后,随机返回
{ confirm: 'confirm' }到sendTopic2(/topic2/${mac}); - 前端接收响应,标记该硬件为 “已匹配”,完成匹配。
流程 3:手动调试
在模拟硬件的命令行输入任意内容(如 { temperature: 25, humidity: 60 }),按回车发送;
前端订阅 sendTopic1(/topic2/1111111),可接收该测试数据,验证前端数据展示逻辑。
四、优化建议与扩展功能
这份模拟代码已能满足基础联调需求,若需更贴近真实硬件,可考虑以下优化:
1. 增加硬件状态模拟
模拟真实硬件的运行状态(如在线 / 离线、电量、信号强度),并定期上报:
// 定期上报硬件状态(每 5 秒)
setInterval(() => {
const status = {
mac: mac,
online: true,
battery: Math.floor(Math.random() * 50) + 50, // 电量 50%-100%
signal: Math.floor(Math.random() * 4) + 1 // 信号 1-4 格
};
sendAsText(sendTopic1, { status: status });
}, 5000);
2. 支持指令参数校验
模拟真实硬件的 “指令合法性校验”,拒绝非法指令(如参数缺失、格式错误):
// 在处理点对点指令时增加校验
if (topic === receiveTopic2) {
const parsedMsg = JSON.parse(textMessage);
// 校验指令是否包含 operation 参数
if (!parsedMsg.operation) {
console.log(`\n非法指令:缺少 operation 参数`);
sendAsText(sendTopic2, { error: 'missing operation' }); // 返回错误
rl.prompt();
return;
}
// 后续处理...
}
3. 记录通讯日志
将所有收发的消息记录到日志文件,便于调试问题:
const fs = require('fs');
const path = require('path');
// 记录日志函数
const logMessage = (type, topic, message) => {
const log = `[${new Date().toISOString()}] [${type}] 主题: ${topic}, 内容: ${message}\n`;
fs.appendFile(path.join(__dirname, 'hardware.log'), log, (err) => {
if (err) console.error('日志写入失败:', err);
});
};
// 发送消息时记录日志
const sendAsText = (topic, text) => {
const message = JSON.stringify(text);
logMessage('发送', topic, message); // 记录发送日志
// 后续发布逻辑...
};
// 接收消息时记录日志
client.on('message', (topic, message) => {
const textMessage = message.toString();
logMessage('接收', topic, textMessage); // 记录接收日志
// 后续处理逻辑...
});
五、总结
这份 Node.js 模拟硬件代码的价值在于:
- 解耦开发:前端无需等待真实硬件,可独立完成 MQTT 通讯逻辑开发和联调;
- 协议验证:提前验证前端与硬件的通讯协议(主题格式、指令结构),避免后期硬件开发完成后才发现协议不兼容;
- 场景模拟:支持随机响应、多设备并发、手动调试等场景,覆盖真实环境中的大部分情况;
- 易于扩展:代码结构清晰,可根据实际需求快速增加状态上报、指令校验、日志记录等功能。
结合上一篇的 Vue3 前端页面和本文的 Node.js 模拟硬件,你已经拥有了一套完整的 “前端 - 硬件 MQTT 通讯” 开发与测试方案。后续只需将模拟硬件的逻辑迁移到真实硬件(如 ESP32、Arduino),即可完成从模拟到实际的无缝过渡。
完整代码
// mqttConfig.js
const mqtt = require('mqtt');
// MQTT 连接配置
const host = '127.0.0.1';
const port = '8080';
const clientId = `mqtt_${Math.random().toString(16).slice(3)}`;
const connectUrl = `mqtt://${host}:${port}`;
// 公共连接参数
const connectOptions = {
clientId,
clean: true,
connectTimeout: 4000,
reconnectPeriod: 1000,
};
// 导出 MQTT 库和连接配置,方便其他文件使用
module.exports = {
mqtt,
connectUrl,
connectOptions,
};// 设备
const { mqtt, connectUrl, connectOptions } = require('./mqttConfig.js');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '请输入要发送的数据 (按回车发送,输入"exit"退出): '
});
const mac = generateRandomMac()
const sendTopic1 = '/topic2/1111111';
const receiveTopic1 = '/topic1/1111111';
const sendTopic2 = `/topic2/${mac}`;
const receiveTopic2 = `/topic1/${mac}`;
const client = mqtt.connect(connectUrl, connectOptions);
//随机MAC地址
function generateRandomMac() {
const hexChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let mac = '';
for (let i = 0; i < 6; i++) {
const char1 = hexChars[Math.floor(Math.random() * 16)];
const char2 = hexChars[Math.floor(Math.random() * 16)];
mac += char1 + char2;
if (i < 5) {
mac += ':';
}
}
return mac;
}
const sendAsText = (topic, text) => {
const message = JSON.stringify(text);
client.publish(topic, message, { qos: 0, retain: false }, (err) => {
if (err) {
console.error('消息发布失败:', err);
} else {
console.log(`\n已发送消息++++++++++++++++++++++++++++++++`);
}
});
};
client.on('connect', () => {
// 订阅页面发送的主题 客户级别
client.subscribe(receiveTopic1, (err) => {
if (err) {
console.error('订阅主题失败:', err);
} else {
console.log(`已订阅页面发送的主题: ${receiveTopic1}`);
rl.prompt();
}
});
// 订阅页面发送的主题 点对点
client.subscribe(receiveTopic2, (err) => {
if (err) {
console.error('订阅主题失败:', err);
} else {
console.log(`\n已订阅页面发送的主题: ${receiveTopic2}`);
rl.prompt();
}
});
});
// 监听来自页面的消息
client.on('message', (topic, message) => {
if (topic === receiveTopic1) {
const textMessage = message.toString();
//生成随机数
const randomNumber = Math.floor(Math.random() * 100000);
const rander = Math.floor(Math.random() * 10);
if (randomNumber % rander === 0) {
//模拟需要匹配的硬件
if (JSON.parse(textMessage).status === 'configgateway') {
sendAsText(sendTopic1, { config: mac });
}
}
rl.prompt();
}
if (topic === receiveTopic2) {
const textMessage = message.toString();
console.log(`\n收到配网消息-------------------------- `);
if (JSON.parse(textMessage).operation === 'pressdown') {
const randomNumber = Math.floor(Math.random() * 100000);
const rander = Math.floor(Math.random() * 10);
if (randomNumber % rander === 0) {
//模拟硬件响应,给页面回应
console.log(`\n已按下硬件按键++++++++++++++++++++`);
sendAsText(sendTopic2, { confirm: 'confirm' });
}
}
rl.prompt();
}
});
// 处理命令行输入
rl.on('line', (input) => {
const message = input.trim();
if (message.toLowerCase() === 'exit') {
console.log('正在断开连接...');
client.end(() => {
console.log('已断开连接,程序退出');
process.exit(0);
});
return;
}
if (message) {
// 使用专门的文本发送函数
sendAsText(sendTopic1, message);
rl.prompt();
} else {
rl.prompt();
}
});
client.on('error', (err) => {
console.error('设备连接错误:', err);
rl.close();
});
client.on('close', () => {
console.log('MQTT连接已关闭');
});
2075

被折叠的 条评论
为什么被折叠?



