用 Node.js 模拟 MQTT 硬件设备:从配置到通讯全解析

在上一篇博客中,我们实现了 Vue3 前端页面与硬件设备的 MQTT 通讯交互。但实际开发中,硬件设备往往需要等待生产或调试,此时用 Node.js 模拟硬件设备 就成了高效开发的关键 —— 它能模拟硬件的 MQTT 连接、消息接收、指令响应等核心行为,让前端开发无需依赖真实硬件即可完成联调。

本文将详细解析这份 Node.js 硬件模拟代码,从配置设计、核心功能到通讯逻辑,帮你理解 “模拟硬件” 如何与前端页面实现完整交互,并提供优化建议和实战技巧。

一、模拟硬件的核心目标

在 IoT 通讯场景中,硬件设备的核心行为包括:

  1. 与 MQTT 服务器建立稳定连接;
  2. 订阅前端发送的指令主题(如 “搜索硬件”“匹配硬件” 指令);
  3. 按约定协议响应前端指令(如返回硬件 MAC 地址、确认匹配成功);
  4. 支持手动发送测试数据,方便调试;
  5. 处理连接异常、主动断开等边界场景。

这份 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. 准备工作

  1. 启动 MQTT 服务器(如 EMQ X、Mosquitto,确保地址和端口与 mqttConfig.js 一致);
  2. 确保前端页面的 MQTT 配置(服务器地址、主题格式)与模拟硬件一致(如前端通讯关键字为 1111111,与模拟硬件的 receiveTopic1 匹配)。

2. 启动模拟硬件

# 1. 安装依赖(若未安装)
npm install mqtt

# 2. 启动模拟设备(可同时启动多个,测试多硬件场景)
node 你的硬件模拟文件名.js

启动后,控制台会显示:

  • 硬件 MAC 地址;
  • MQTT 连接成功提示;
  • 已订阅的主题;
  • 命令行输入提示。

3. 前端联调流程

流程 1:搜索硬件
  1. 在 Vue3 页面点击「匹配硬件」按钮;
  2. 前端发送 { status: 'configgateway' } 到 receiveTopic1/topic1/1111111);
  3. 模拟硬件收到指令后,随机返回 { config: mac } 到 sendTopic1/topic2/1111111);
  4. 前端接收响应,将 MAC 地址加入硬件列表,完成搜索。
流程 2:匹配硬件
  1. 前端搜索到硬件列表后,自动逐个发送 { operation: 'pressdown' } 到每个硬件的点对点主题(/topic1/${mac});
  2. 模拟硬件收到指令后,随机返回 { confirm: 'confirm' } 到 sendTopic2/topic2/${mac});
  3. 前端接收响应,标记该硬件为 “已匹配”,完成匹配。
流程 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 模拟硬件代码的价值在于:

  1. 解耦开发:前端无需等待真实硬件,可独立完成 MQTT 通讯逻辑开发和联调;
  2. 协议验证:提前验证前端与硬件的通讯协议(主题格式、指令结构),避免后期硬件开发完成后才发现协议不兼容;
  3. 场景模拟:支持随机响应、多设备并发、手动调试等场景,覆盖真实环境中的大部分情况;
  4. 易于扩展:代码结构清晰,可根据实际需求快速增加状态上报、指令校验、日志记录等功能。

结合上一篇的 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连接已关闭');
});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值