【项目思路】基于STM32的工业电机状态监测与远程预警系统:振动+温度采集、Modbus RTU通信、ESP8266 MQTT上云、PyQt5上位机
·
一、项目概述
1.1 项目背景与目标
在工业生产中,电机是最核心的动力设备。据统计,电机故障导致的非计划停机占工厂总停机时间的30%以上,其中轴承磨损和过热是两大主要故障源。传统的电机维护方式依赖人工巡检,存在以下痛点:
- 人工巡检周期长,无法实时发现早期故障征兆
- 依赖经验判断,缺乏量化数据支撑
- 故障发现滞后,往往已经造成设备损坏或停产
- 多台设备分散部署,巡检人力成本高
本项目设计一套工业电机状态监测与远程预警系统,通过振动传感器和温度传感器实时采集电机运行数据,STM32作为边缘采集节点通过Modbus RTU协议上报数据,ESP8266网关将数据通过MQTT协议上传至云端,配合PyQt5上位机实现远程监控、趋势分析和故障预警。
1.2 技术栈选型
| 技术领域 | 具体选型 | 选择理由 |
|---|---|---|
| 采集节点MCU | STM32F103C8T6 | 成本低、ADC精度满足需求、Modbus生态成熟 |
| 振动传感器 | ADXL345(三轴加速度计) | I2C/SPI双接口、16g量程、13位分辨率 |
| 温度传感器 | DS18B20 | 单总线接口、±0.5℃精度、无需校准 |
| 现场总线 | Modbus RTU(RS485) | 工业标准协议、抗干扰强、支持多从站 |
| RS485收发器 | MAX485 | 经典方案、半双工、最大通信距离1200m |
| 网关 | ESP8266 NodeMCU | WiFi内置、成本低、Arduino生态丰富 |
| 云端协议 | MQTT(EMQX Broker) | 轻量级、适合IoT场景、支持QoS |
| 上位机 | Python 3.x + PyQt5 | 开发效率高、pyqtgraph实时绘图性能好 |
| 数据库 | SQLite | 轻量级、无需部署、适合单机监控场景 |
1.3 系统功能
- 实时采集电机振动加速度(X/Y/Z三轴)和表面温度
- STM32采集节点通过Modbus RTU协议上报数据,支持多台电机级联
- ESP8266网关汇聚Modbus数据,通过MQTT上传至云端Broker
- PyQt5上位机实时展示振动波形、温度趋势、设备状态
- 多级报警机制:振动超限预警、温度过高报警、设备离线告警
- SQLite历史数据存储,支持趋势回溯与导出
二、系统架构设计
2.1 整体架构

系统采用边缘采集 + 网关汇聚 + 云端监控的三层架构:
- 感知层:STM32 + ADXL345 + DS18B20,负责数据采集与Modbus RTU从站响应
- 传输层:ESP8266网关作为Modbus RTU主站,轮询各采集节点,通过WiFi/MQTT上传
- 应用层:EMQX Broker + PyQt5上位机,负责数据接收、可视化、报警与存储
2.2 通信架构

- 采集节点与网关之间:Modbus RTU over RS485(半双工,9600bps)
- 网关与云端之间:MQTT over WiFi(QoS 1,保证至少一次送达)
三、硬件设计与连接
3.1 硬件清单
| 组件 | 型号 | 数量 | 说明 |
|---|---|---|---|
| 采集节点MCU | STM32F103C8T6 | 1~N | 每台电机一个采集节点 |
| 振动传感器 | ADXL345 | 1/节点 | 贴装在电机外壳 |
| 温度传感器 | DS18B20 | 1/节点 | 贴装在电机表面 |
| RS485收发器 | MAX485 | 1/节点+1 | Modbus RTU物理层 |
| 网关 | ESP8266 NodeMCU | 1 | Modbus主站+MQTT网关 |
| 电源 | 5V/3.3V | 若干 | 现场供电 |
3.2 采集节点引脚分配(STM32F103C8T6)
ADXL345: SCL -> PB6 (I2C1_SCL)
SDA -> PB7 (I2C1_SDA)
INT1 -> PA0 (外部中断,数据就绪)
VCC -> 3.3V
DS18B20: DATA -> PA1 (GPIO,外接4.7K上拉)
VCC -> 3.3V
MAX485: DI -> PA9 (USART1_TX)
RO -> PA10 (USART1_RX)
DE -> PA8 (GPIO,发送使能)
RE -> PA8 (与DE短接)
A/B -> RS485总线
3.3 网关引脚分配(ESP8266 NodeMCU)
MAX485: DI -> GPIO1 (UART TX)
RO -> GPIO3 (UART RX)
DE -> GPIO5 (D1,发送使能)
RE -> GPIO5 (与DE短接)
A/B -> RS485总线
重要提醒:
- MAX485的DE和RE引脚短接,高电平发送、低电平接收
- RS485总线两端各接120欧姆终端电阻
- ADXL345的SDO引脚接GND时I2C地址为0x53,接VCC时为0x1D
- DS18B20的DATA引脚必须外接4.7K上拉电阻
四、代码实现详解
4.1 ADXL345振动传感器驱动
// adxl345.h
#ifndef __ADXL345_H
#define __ADXL345_H
#include "stm32f1xx_hal.h"
#include <math.h>
#define ADXL345_ADDR (0x53 << 1) // SDO接GND
#define ADXL345_DEVID 0x00
#define ADXL345_POWER_CTL 0x2D
#define ADXL345_DATA_FORMAT 0x31
#define ADXL345_BW_RATE 0x2C
#define ADXL345_DATAX0 0x32
typedef struct {
float x; // X轴加速度 (g)
float y; // Y轴加速度 (g)
float z; // Z轴加速度 (g)
float rms; // 合成振动有效值 (g)
} ADXL345_Data;
HAL_StatusTypeDef ADXL345_Init(I2C_HandleTypeDef *hi2c);
HAL_StatusTypeDef ADXL345_ReadData(I2C_HandleTypeDef *hi2c, ADXL345_Data *data);
#endif
// adxl345.c
#include "adxl345.h"
static HAL_StatusTypeDef ADXL345_WriteReg(I2C_HandleTypeDef *hi2c, uint8_t reg, uint8_t val) {
uint8_t buf[2] = {reg, val};
return HAL_I2C_Master_Transmit(hi2c, ADXL345_ADDR, buf, 2, 100);
}
static HAL_StatusTypeDef ADXL345_ReadRegs(I2C_HandleTypeDef *hi2c, uint8_t reg, uint8_t *buf, uint16_t len) {
HAL_StatusTypeDef ret;
ret = HAL_I2C_Master_Transmit(hi2c, ADXL345_ADDR, ®, 1, 100);
if (ret != HAL_OK) return ret;
return HAL_I2C_Master_Receive(hi2c, ADXL345_ADDR, buf, len, 100);
}
HAL_StatusTypeDef ADXL345_Init(I2C_HandleTypeDef *hi2c) {
uint8_t id = 0;
ADXL345_ReadRegs(hi2c, ADXL345_DEVID, &id, 1);
if (id != 0xE5) return HAL_ERROR; // 器件ID校验失败
// 数据格式:全分辨率模式,+-16g量程
ADXL345_WriteReg(hi2c, ADXL345_DATA_FORMAT, 0x0B);
// 采样率100Hz(满足工业电机50Hz基频的振动检测)
ADXL345_WriteReg(hi2c, ADXL345_BW_RATE, 0x0A);
// 进入测量模式
ADXL345_WriteReg(hi2c, ADXL345_POWER_CTL, 0x08);
return HAL_OK;
}
HAL_StatusTypeDef ADXL345_ReadData(I2C_HandleTypeDef *hi2c, ADXL345_Data *data) {
uint8_t raw[6];
HAL_StatusTypeDef ret = ADXL345_ReadRegs(hi2c, ADXL345_DATAX0, raw, 6);
if (ret != HAL_OK) return ret;
// 原始值转换(全分辨率模式下,3.9mg/LSB)
int16_t raw_x = (int16_t)(raw[1] << 8 | raw[0]);
int16_t raw_y = (int16_t)(raw[3] << 8 | raw[2]);
int16_t raw_z = (int16_t)(raw[5] << 8 | raw[4]);
data->x = raw_x * 0.0039f;
data->y = raw_y * 0.0039f;
data->z = raw_z * 0.0039f;
// 计算合成振动RMS值(去除重力分量)
float vx = data->x;
float vy = data->y;
float vz = data->z - 1.0f; // Z轴减去1g重力
data->rms = sqrtf(vx * vx + vy * vy + vz * vz);
return HAL_OK;
}
4.2 DS18B20温度传感器驱动
// ds18b20.h
#ifndef __DS18B20_H
#define __DS18B20_H
#include "stm32f1xx_hal.h"
#define DS18B20_PORT GPIOA
#define DS18B20_PIN GPIO_PIN_1
HAL_StatusTypeDef DS18B20_Init(void);
float DS18B20_ReadTemp(void);
#endif
// ds18b20.c
#include "ds18b20.h"
static void DS18B20_SetOutput(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DS18B20_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
}
static void DS18B20_SetInput(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DS18B20_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
}
// 微秒延时(基于DWT周期计数器)
static void Delay_us(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000);
while ((DWT->CYCCNT - start) < ticks);
}
static uint8_t DS18B20_Reset(void) {
DS18B20_SetOutput();
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
Delay_us(480); // 拉低至少480us
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
DS18B20_SetInput();
Delay_us(60); // 等待60us后检测存在脉冲
uint8_t presence = HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN);
Delay_us(420);
return (presence == GPIO_PIN_RESET) ? HAL_OK : HAL_ERROR;
}
static void DS18B20_WriteByte(uint8_t byte) {
DS18B20_SetOutput();
for (int i = 0; i < 8; i++) {
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
Delay_us(2);
if (byte & 0x01) {
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
}
Delay_us(60);
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
Delay_us(2);
byte >>= 1;
}
}
static uint8_t DS18B20_ReadByte(void) {
uint8_t byte = 0;
for (int i = 0; i < 8; i++) {
DS18B20_SetOutput();
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
Delay_us(2);
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
DS18B20_SetInput();
Delay_us(12);
if (HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN)) {
byte |= (1 << i);
}
Delay_us(50);
}
return byte;
}
HAL_StatusTypeDef DS18B20_Init(void) {
// 启用DWT周期计数器
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
return DS18B20_Reset() == HAL_OK ? HAL_OK : HAL_ERROR;
}
float DS18B20_ReadTemp(void) {
if (DS18B20_Reset() != HAL_OK) return -999.0f;
DS18B20_WriteByte(0xCC); // 跳过ROM(单设备)
DS18B20_WriteByte(0x44); // 启动温度转换
HAL_Delay(750); // 12位精度需要750ms
if (DS18B20_Reset() != HAL_OK) return -999.0f;
DS18B20_WriteByte(0xCC);
DS18B20_WriteByte(0xBE); // 读取暂存器
uint8_t lsb = DS18B20_ReadByte();
uint8_t msb = DS18B20_ReadByte();
int16_t raw = (msb << 8) | lsb;
return raw * 0.0625f; // 12位精度:0.0625℃/LSB
}
4.3 Modbus RTU从站实现
// modbus_slave.h
#ifndef __MODBUS_SLAVE_H
#define __MODBUS_SLAVE_H
#include "stm32f1xx_hal.h"
#define MODBUS_SLAVE_ADDR 0x01 // 从站地址(每台电机不同)
#define MODBUS_BAUD 9600
// 保持寄存器地址映射
// 0x0000: 振动X轴 (x100, int16)
// 0x0001: 振动Y轴 (x100, int16)
// 0x0002: 振动Z轴 (x100, int16)
// 0x0003: 振动RMS (x100, uint16)
// 0x0004: 温度 (x10, int16)
// 0x0005: 设备状态 (0=正常, 1=预警, 2=报警, 3=严重故障)
#define REG_VIBRATION_X 0x0000
#define REG_VIBRATION_Y 0x0001
#define REG_VIBRATION_Z 0x0002
#define REG_VIBRATION_RMS 0x0003
#define REG_TEMPERATURE 0x0004
#define REG_DEVICE_STATUS 0x0005
#define REG_COUNT 6
void Modbus_Init(UART_HandleTypeDef *huart);
void Modbus_Poll(void);
void Modbus_UpdateRegisters(float vib_x, float vib_y, float vib_z, float vib_rms, float temp);
#endif
// modbus_slave.c
#include "modbus_slave.h"
#include <string.h>
static UART_HandleTypeDef *modbus_uart;
static uint16_t holding_regs[REG_COUNT] = {0};
static uint8_t rx_buf[256];
static uint8_t tx_buf[256];
// RS485方向控制
#define RS485_DE_PORT GPIOA
#define RS485_DE_PIN GPIO_PIN_8
#define RS485_TX_MODE() HAL_GPIO_WritePin(RS485_DE_PORT, RS485_DE_PIN, GPIO_PIN_SET)
#define RS485_RX_MODE() HAL_GPIO_WritePin(RS485_DE_PORT, RS485_DE_PIN, GPIO_PIN_RESET)
// Modbus CRC16标准校验
static uint16_t Modbus_CRC16(uint8_t *buf, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= buf[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
void Modbus_Init(UART_HandleTypeDef *huart) {
modbus_uart = huart;
// 配置RS485方向控制引脚
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = RS485_DE_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(RS485_DE_PORT, &GPIO_InitStruct);
RS485_RX_MODE(); // 默认接收模式
memset(holding_regs, 0, sizeof(holding_regs));
}
void Modbus_UpdateRegisters(float vib_x, float vib_y, float vib_z, float vib_rms, float temp) {
// 浮点转定点存储(振动x100,温度x10)
holding_regs[REG_VIBRATION_X] = (int16_t)(vib_x * 100);
holding_regs[REG_VIBRATION_Y] = (int16_t)(vib_y * 100);
holding_regs[REG_VIBRATION_Z] = (int16_t)(vib_z * 100);
holding_regs[REG_VIBRATION_RMS] = (uint16_t)(vib_rms * 100);
holding_regs[REG_TEMPERATURE] = (int16_t)(temp * 10);
// 多级状态判断
uint16_t status = 0; // 默认正常
if (vib_rms > 4.5f) status = 3; // 严重故障:振动>4.5g
else if (vib_rms > 2.8f) status = 1; // 振动预警:>2.8g
if (temp > 85.0f) status = (status < 2) ? 2 : 3; // 温度报警:>85℃
else if (temp > 70.0f && status == 0) status = 1; // 温度预警:>70℃
holding_regs[REG_DEVICE_STATUS] = status;
}
// 处理功能码0x03:读保持寄存器
static void Modbus_HandleReadHolding(uint8_t *frame, uint16_t len) {
uint16_t start_addr = (frame[2] << 8) | frame[3];
uint16_t reg_count = (frame[4] << 8) | frame[5];
if (start_addr + reg_count > REG_COUNT) return;
uint8_t resp_len = 0;
tx_buf[resp_len++] = MODBUS_SLAVE_ADDR;
tx_buf[resp_len++] = 0x03;
tx_buf[resp_len++] = reg_count * 2;
for (uint16_t i = 0; i < reg_count; i++) {
tx_buf[resp_len++] = holding_regs[start_addr + i] >> 8;
tx_buf[resp_len++] = holding_regs[start_addr + i] & 0xFF;
}
uint16_t crc = Modbus_CRC16(tx_buf, resp_len);
tx_buf[resp_len++] = crc & 0xFF;
tx_buf[resp_len++] = crc >> 8;
RS485_TX_MODE();
HAL_UART_Transmit(modbus_uart, tx_buf, resp_len, 100);
RS485_RX_MODE();
}
void Modbus_Poll(void) {
RS485_RX_MODE();
if (HAL_UART_Receive(modbus_uart, rx_buf, 8, 100) != HAL_OK) return;
// 校验从站地址
if (rx_buf[0] != MODBUS_SLAVE_ADDR) return;
// CRC校验
uint16_t rx_crc = (rx_buf[7] << 8) | rx_buf[6];
uint16_t calc_crc = Modbus_CRC16(rx_buf, 6);
if (rx_crc != calc_crc) return;
// 功能码分发
switch (rx_buf[1]) {
case 0x03: Modbus_HandleReadHolding(rx_buf, 8); break;
default: break;
}
}
4.4 STM32采集节点主程序
// main.c — STM32采集节点
#include "stm32f1xx_hal.h"
#include "adxl345.h"
#include "ds18b20.h"
#include "modbus_slave.h"
extern I2C_HandleTypeDef hi2c1;
extern UART_HandleTypeDef huart1;
ADXL345_Data g_vib_data;
float g_temperature = 0.0f;
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init(); // RS485 Modbus
// 启用DWT周期计数器(微秒延时用)
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
// 初始化传感器和Modbus
ADXL345_Init(&hi2c1);
DS18B20_Init();
Modbus_Init(&huart1);
uint32_t last_sample = 0;
while (1) {
// 处理Modbus请求(优先级最高)
Modbus_Poll();
// 每500ms采集一次传感器数据
if (HAL_GetTick() - last_sample >= 500) {
last_sample = HAL_GetTick();
ADXL345_ReadData(&hi2c1, &g_vib_data);
g_temperature = DS18B20_ReadTemp();
// 更新Modbus寄存器
Modbus_UpdateRegisters(
g_vib_data.x, g_vib_data.y, g_vib_data.z,
g_vib_data.rms, g_temperature
);
}
}
}
4.5 ESP8266网关固件
// gateway.ino — ESP8266 Modbus RTU主站 + MQTT网关
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <SoftwareSerial.h>
const char* WIFI_SSID = "YourSSID";
const char* WIFI_PASS = "YourPassword";
const char* MQTT_SERVER = "broker.emqx.io";
const int MQTT_PORT = 1883;
const char* MQTT_TOPIC = "industrial/motor/data";
const char* MQTT_ALARM = "industrial/motor/alarm";
#define RS485_DE_PIN 5 // GPIO5 (D1)
SoftwareSerial rs485Serial(13, 15); // RX=D7, TX=D8
WiFiClient espClient;
PubSubClient mqtt(espClient);
#define POLL_INTERVAL 2000
#define MAX_SLAVES 3
struct SlaveConfig {
uint8_t addr;
const char* name;
bool online;
unsigned long lastSeen;
};
SlaveConfig slaves[MAX_SLAVES] = {
{0x01, "motor_1", false, 0},
{0x02, "motor_2", false, 0},
{0x03, "motor_3", false, 0}
};
uint16_t modbusCRC16(uint8_t *buf, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= buf[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) { crc >>= 1; crc ^= 0xA001; }
else { crc >>= 1; }
}
}
return crc;
}
// Modbus主站:读保持寄存器
bool modbusReadHolding(uint8_t slaveAddr, uint16_t startReg, uint16_t regCount, uint16_t *result) {
uint8_t request[8];
request[0] = slaveAddr;
request[1] = 0x03;
request[2] = startReg >> 8;
request[3] = startReg & 0xFF;
request[4] = regCount >> 8;
request[5] = regCount & 0xFF;
uint16_t crc = modbusCRC16(request, 6);
request[6] = crc & 0xFF;
request[7] = crc >> 8;
// 发送请求
digitalWrite(RS485_DE_PIN, HIGH);
delay(1);
rs485Serial.write(request, 8);
rs485Serial.flush();
delay(1);
digitalWrite(RS485_DE_PIN, LOW);
// 等待响应
unsigned long start = millis();
uint8_t expected = 3 + regCount * 2 + 2;
uint8_t response[64];
uint8_t idx = 0;
while (millis() - start < 200 && idx < expected) {
if (rs485Serial.available()) {
response[idx++] = rs485Serial.read();
}
}
if (idx < expected) return false;
// CRC校验
uint16_t rxCrc = (response[idx-1] << 8) | response[idx-2];
if (rxCrc != modbusCRC16(response, idx - 2)) return false;
// 解析寄存器数据
for (uint16_t i = 0; i < regCount; i++) {
result[i] = (response[3 + i*2] << 8) | response[3 + i*2 + 1];
}
return true;
}
void pollAndPublish() {
for (int i = 0; i < MAX_SLAVES; i++) {
uint16_t regs[6] = {0};
bool ok = modbusReadHolding(slaves[i].addr, 0x0000, 6, regs);
if (ok) {
slaves[i].online = true;
slaves[i].lastSeen = millis();
float vib_x = (int16_t)regs[0] / 100.0f;
float vib_y = (int16_t)regs[1] / 100.0f;
float vib_z = (int16_t)regs[2] / 100.0f;
float vib_rms = regs[3] / 100.0f;
float temp = (int16_t)regs[4] / 10.0f;
uint16_t status = regs[5];
// 构建JSON并发布
JsonDocument doc;
doc["device"] = slaves[i].name;
doc["vib_x"] = round(vib_x * 100) / 100.0;
doc["vib_y"] = round(vib_y * 100) / 100.0;
doc["vib_z"] = round(vib_z * 100) / 100.0;
doc["vib_rms"] = round(vib_rms * 100) / 100.0;
doc["temp"] = round(temp * 10) / 10.0;
doc["status"] = status;
char payload[256];
serializeJson(doc, payload);
mqtt.publish(MQTT_TOPIC, payload);
// 报警推送
if (status >= 2) {
JsonDocument alarmDoc;
alarmDoc["device"] = slaves[i].name;
alarmDoc["level"] = (status == 3) ? "critical" : "warning";
alarmDoc["vib_rms"] = vib_rms;
alarmDoc["temp"] = temp;
char alarmPayload[200];
serializeJson(alarmDoc, alarmPayload);
mqtt.publish(MQTT_ALARM, alarmPayload);
}
} else {
// 设备离线检测
if (slaves[i].online && millis() - slaves[i].lastSeen > 10000) {
slaves[i].online = false;
JsonDocument offDoc;
offDoc["device"] = slaves[i].name;
offDoc["level"] = "offline";
char offPayload[128];
serializeJson(offDoc, offPayload);
mqtt.publish(MQTT_ALARM, offPayload);
}
}
delay(50);
}
}
void connectMQTT() {
while (!mqtt.connected()) {
String clientId = "motor_gw_" + String(random(0xffff), HEX);
if (mqtt.connect(clientId.c_str())) {
Serial.println("[MQTT] Connected");
} else {
delay(3000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(RS485_DE_PIN, OUTPUT);
digitalWrite(RS485_DE_PIN, LOW);
rs485Serial.begin(9600);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
Serial.printf("[WiFi] IP: %s\n", WiFi.localIP().toString().c_str());
mqtt.setServer(MQTT_SERVER, MQTT_PORT);
mqtt.setBufferSize(512);
connectMQTT();
}
void loop() {
if (!mqtt.connected()) connectMQTT();
mqtt.loop();
static unsigned long lastPoll = 0;
if (millis() - lastPoll >= POLL_INTERVAL) {
lastPoll = millis();
pollAndPublish();
}
}
4.6 PyQt5上位机实现
MQTT客户端与数据存储
# mqtt_client.py
import json
import sqlite3
import threading
from datetime import datetime
import paho.mqtt.client as mqtt
from PyQt5.QtCore import QObject, pyqtSignal
class MotorMQTTClient(QObject):
data_received = pyqtSignal(dict)
alarm_received = pyqtSignal(dict)
connection_changed = pyqtSignal(bool)
def __init__(self, broker='broker.emqx.io', port=1883):
super().__init__()
self.broker = broker
self.port = port
self.client = mqtt.Client(client_id=f'motor_monitor_{id(self)}')
self.client.on_connect = self._on_connect
self.client.on_message = self._on_message
self.client.on_disconnect = self._on_disconnect
self.db = sqlite3.connect('motor_data.db', check_same_thread=False)
self.db_lock = threading.Lock()
self._init_db()
def _init_db(self):
cursor = self.db.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS motor_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device TEXT,
vib_x REAL, vib_y REAL, vib_z REAL, vib_rms REAL,
temperature REAL, status INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS alarm_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device TEXT, level TEXT,
vib_rms REAL, temperature REAL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
self.db.commit()
def _on_connect(self, client, userdata, flags, rc):
if rc == 0:
client.subscribe('industrial/motor/data')
client.subscribe('industrial/motor/alarm')
self.connection_changed.emit(True)
def _on_disconnect(self, client, userdata, rc):
self.connection_changed.emit(False)
def _on_message(self, client, userdata, msg):
try:
data = json.loads(msg.payload.decode())
data['recv_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if msg.topic == 'industrial/motor/data':
self._save_data(data)
self.data_received.emit(data)
elif msg.topic == 'industrial/motor/alarm':
self._save_alarm(data)
self.alarm_received.emit(data)
except Exception as e:
print(f'Message parse error: {e}')
def _save_data(self, data):
with self.db_lock:
self.db.execute(
'INSERT INTO motor_data (device,vib_x,vib_y,vib_z,vib_rms,temperature,status) VALUES (?,?,?,?,?,?,?)',
(data.get('device'), data.get('vib_x',0), data.get('vib_y',0),
data.get('vib_z',0), data.get('vib_rms',0), data.get('temp',0), data.get('status',0)))
self.db.commit()
def _save_alarm(self, data):
with self.db_lock:
self.db.execute(
'INSERT INTO alarm_log (device,level,vib_rms,temperature) VALUES (?,?,?,?)',
(data.get('device'), data.get('level'), data.get('vib_rms',0), data.get('temp',0)))
self.db.commit()
def start(self):
self.client.connect_async(self.broker, self.port)
self.client.loop_start()
def stop(self):
self.client.loop_stop()
self.client.disconnect()
self.db.close()
监控主界面
# main_window.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import pyqtgraph as pg
from mqtt_client import MotorMQTTClient
class MotorMonitor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('工业电机状态监测系统')
self.setGeometry(100, 100, 1400, 900)
self.setStyleSheet('background-color: #1a1a2e; color: #e0e0e0;')
self.history = {}
self.max_points = 300
self._init_ui()
self._init_mqtt()
def _init_ui(self):
central = QWidget()
self.setCentralWidget(central)
main_layout = QVBoxLayout(central)
# 顶部标题栏
header = QHBoxLayout()
title = QLabel('工业电机状态监测系统')
title.setStyleSheet('font-size: 20px; font-weight: bold; color: #00d2ff; padding: 8px;')
header.addWidget(title)
self.conn_label = QLabel('MQTT: 断开')
self.conn_label.setStyleSheet('font-size: 13px; color: #ff6b6b; padding: 8px;')
header.addStretch()
header.addWidget(self.conn_label)
main_layout.addLayout(header)
content = QHBoxLayout()
main_layout.addLayout(content)
# 左侧:设备状态卡片
left_panel = QVBoxLayout()
content.addLayout(left_panel, 1)
self.device_cards = {}
for dev_id, dev_name in [('motor_1','1号电机'), ('motor_2','2号电机'), ('motor_3','3号电机')]:
card = self._create_device_card(dev_id, dev_name)
left_panel.addWidget(card['widget'])
self.device_cards[dev_id] = card
left_panel.addStretch()
# 右侧:实时曲线
right_panel = QVBoxLayout()
content.addLayout(right_panel, 3)
# 振动RMS趋势
self.vib_plot = pg.PlotWidget(title='振动RMS趋势 (g)')
self.vib_plot.setBackground('#16213e')
self.vib_plot.showGrid(x=True, y=True, alpha=0.3)
self.vib_plot.addLegend()
self.vib_curves = {}
colors = {'motor_1': '#00d2ff', 'motor_2': '#ff6b6b', 'motor_3': '#ffd93d'}
for dev_id, color in colors.items():
self.vib_curves[dev_id] = self.vib_plot.plot(pen=pg.mkPen(color, width=2), name=dev_id)
self.vib_plot.addLine(y=2.8, pen=pg.mkPen('#ffd93d', width=1, style=Qt.DashLine))
self.vib_plot.addLine(y=4.5, pen=pg.mkPen('#ff6b6b', width=1, style=Qt.DashLine))
right_panel.addWidget(self.vib_plot)
# 温度趋势
self.temp_plot = pg.PlotWidget(title='温度趋势 (℃)')
self.temp_plot.setBackground('#16213e')
self.temp_plot.showGrid(x=True, y=True, alpha=0.3)
self.temp_plot.addLegend()
self.temp_curves = {}
for dev_id, color in colors.items():
self.temp_curves[dev_id] = self.temp_plot.plot(pen=pg.mkPen(color, width=2), name=dev_id)
self.temp_plot.addLine(y=70, pen=pg.mkPen('#ffd93d', width=1, style=Qt.DashLine))
self.temp_plot.addLine(y=85, pen=pg.mkPen('#ff6b6b', width=1, style=Qt.DashLine))
right_panel.addWidget(self.temp_plot)
# 底部:报警日志
self.alarm_list = QListWidget()
self.alarm_list.setMaximumHeight(120)
self.alarm_list.setStyleSheet(
'QListWidget { background-color: #16213e; border: 1px solid #333; font-size: 12px; }'
)
main_layout.addWidget(QLabel('报警日志:'))
main_layout.addWidget(self.alarm_list)
def _create_device_card(self, dev_id, dev_name):
widget = QFrame()
widget.setStyleSheet('''
QFrame { background-color: #16213e; border: 1px solid #333;
border-radius: 8px; padding: 10px; margin: 4px; }
''')
layout = QVBoxLayout(widget)
name_label = QLabel(dev_name)
name_label.setStyleSheet('font-size: 15px; font-weight: bold; color: #00d2ff;')
status_label = QLabel('离线')
status_label.setStyleSheet('font-size: 12px; color: #888;')
vib_label = QLabel('振动: -- g')
vib_label.setStyleSheet('font-size: 18px; font-weight: bold;')
temp_label = QLabel('温度: -- ℃')
temp_label.setStyleSheet('font-size: 18px; font-weight: bold;')
layout.addWidget(name_label)
layout.addWidget(status_label)
layout.addWidget(vib_label)
layout.addWidget(temp_label)
return {'widget': widget, 'status_label': status_label,
'vib_label': vib_label, 'temp_label': temp_label}
def _init_mqtt(self):
self.mqtt_client = MotorMQTTClient()
self.mqtt_client.data_received.connect(self.on_data)
self.mqtt_client.alarm_received.connect(self.on_alarm)
self.mqtt_client.connection_changed.connect(self.on_connection)
self.mqtt_client.start()
@pyqtSlot(bool)
def on_connection(self, connected):
if connected:
self.conn_label.setText('MQTT: 已连接')
self.conn_label.setStyleSheet('font-size: 13px; color: #00d2ff; padding: 8px;')
else:
self.conn_label.setText('MQTT: 断开')
self.conn_label.setStyleSheet('font-size: 13px; color: #ff6b6b; padding: 8px;')
@pyqtSlot(dict)
def on_data(self, data):
dev = data.get('device', '')
if dev not in self.device_cards:
return
card = self.device_cards[dev]
vib_rms = data.get('vib_rms', 0)
temp = data.get('temp', 0)
status = data.get('status', 0)
card['vib_label'].setText(f'振动: {vib_rms:.2f} g')
card['temp_label'].setText(f'温度: {temp:.1f} ℃')
status_map = {0: ('运行正常', '#00d2ff'), 1: ('预警', '#ffd93d'),
2: ('报警', '#ff6b6b'), 3: ('严重故障', '#ff0000')}
text, color = status_map.get(status, ('未知', '#888'))
card['status_label'].setText(text)
card['status_label'].setStyleSheet(f'font-size: 12px; color: {color};')
# 更新历史曲线
if dev not in self.history:
self.history[dev] = {'vib_rms': [], 'temp': []}
self.history[dev]['vib_rms'].append(vib_rms)
self.history[dev]['temp'].append(temp)
for k in self.history[dev]:
if len(self.history[dev][k]) > self.max_points:
self.history[dev][k] = self.history[dev][k][-self.max_points:]
if dev in self.vib_curves:
self.vib_curves[dev].setData(self.history[dev]['vib_rms'])
if dev in self.temp_curves:
self.temp_curves[dev].setData(self.history[dev]['temp'])
@pyqtSlot(dict)
def on_alarm(self, data):
dev = data.get('device', '')
level = data.get('level', '')
ts = data.get('recv_time', '')
level_map = {'warning': ('预警','#ffd93d'), 'critical': ('严重','#ff0000'), 'offline': ('离线','#888')}
name, color = level_map.get(level, (level, '#fff'))
msg = f'[{ts}] {dev} - {name}'
if 'vib_rms' in data: msg += f' | 振动:{data["vib_rms"]:.2f}g'
if 'temp' in data: msg += f' | 温度:{data["temp"]:.1f}℃'
item = QListWidgetItem(msg)
item.setForeground(QColor(color))
self.alarm_list.insertItem(0, item)
while self.alarm_list.count() > 100:
self.alarm_list.takeItem(self.alarm_list.count() - 1)
def closeEvent(self, event):
self.mqtt_client.stop()
super().closeEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MotorMonitor()
window.show()
sys.exit(app.exec_())
五、调试与常见问题
5.1 Modbus通信调试
调试RS485通信时,建议先用USB转RS485模块配合Modbus Poll软件验证从站响应:
- 设置串口参数:9600, 8N1
- 功能码03,起始地址0x0000,寄存器数量6
- 正常响应应返回12字节数据(6个寄存器 x 2字节)
常见问题:
- 无响应:检查A/B线是否接反、DE/RE引脚电平是否正确、从站地址是否匹配
- CRC错误:确认双方CRC算法一致(低字节在前)、波特率一致
- 数据乱码:RS485总线两端是否接了120欧终端电阻、线缆是否过长(超过100m建议降低波特率)
5.2 振动传感器安装要点
- ADXL345必须牢固贴装在电机外壳上,建议使用环氧胶或螺丝固定
- 传感器轴向与电机轴对齐,Z轴垂直于安装面
- 避免安装在电机散热风扇附近(气流干扰)
- I2C线缆长度不宜超过50cm,超过建议改用SPI接口
5.3 温度传感器注意事项
- DS18B20探头需紧贴电机外壳,用导热硅脂填充间隙
- 单总线DATA引脚必须接4.7K上拉电阻
- 转换时间750ms(12位精度),采集频率不宜超过1Hz
5.4 ESP8266网关稳定性
- SoftwareSerial在高波特率下不稳定,RS485通信建议不超过9600bps
- MQTT断线重连需要指数退避,避免频繁重连导致Broker封禁
- 建议增加看门狗(ESP.wdtEnable),防止程序卡死
- 内存不足时(heap小于10KB),主动重启清理碎片
六、项目总结与扩展
6.1 项目成果
本项目实现了一套完整的工业电机状态监测与远程预警系统:
- 边缘采集:STM32 + ADXL345 + DS18B20,500ms采样周期,Modbus RTU标准协议上报
- 网关汇聚:ESP8266轮询最多3台从站,MQTT上传至云端Broker
- 远程监控:PyQt5上位机实时展示振动波形、温度趋势、设备状态
- 智能预警:多级报警机制(预警/报警/严重故障/离线),报警日志持久化存储
- 数据存储:SQLite本地数据库,支持历史趋势回溯
6.2 关键技术点
- Modbus RTU协议实现:从站寄存器映射、CRC16校验、RS485半双工控制
- 振动数据处理:三轴加速度采集、RMS合成值计算、阈值分级判断
- MQTT物联网通信:QoS保证、断线重连、JSON数据序列化
- 多设备管理:从站地址编址、设备在线检测、离线告警
6.3 扩展方向
- 增加FFT频谱分析,实现轴承故障特征频率提取
- 接入TensorFlow Lite边缘推理,实现故障模式智能识别
- 替换为工业级振动传感器(IEPE型),提升测量精度和频率范围
- 接入阿里云IoT/华为云IoT,实现手机端远程监控
- 增加FreeRTOS多任务调度,分离采集任务和通信任务
- 加入历史数据导出(CSV/Excel),方便维护人员分析
如果这篇文章对你有帮助,请点赞收藏关注,后续会持续更新更多嵌入式物联网项目实战!
有问题欢迎评论区交流,看到都会回复。
更多推荐
所有评论(0)