一、教程概述
本教程适配CentOS 7虚拟机,全程通过Docker服务部署Node-RED工具,解决CentOS 7原生Node.js版本过低、不兼容新版插件的问题。部署完成后,依托Node-RED Dashboard 2.0插件,搭建包含温湿度、光照、风速、风向的城市气象数据可视化界面,包含完整的环境部署、插件安装、节点配置、代码调试步骤,所有操作通俗易懂,适配新手实操。
核心优势:通过Docker容器化部署,实现配置持久化、串口设备兼容、界面可视化,同时规避旧版插件兼容、权限不足、数据丢失等常见问题。
二、前期准备说明
-
环境基础:CentOS 7虚拟机已开启网络,可正常访问外网,已安装Docker服务(若未安装需先部署Docker)。
-
核心适配:CentOS 7自带Node.js版本过低,无法直接运行Dashboard 2.0插件,因此全程采用Docker镜像部署Node-RED,无需升级系统和本地Node.js版本。
-
工具镜像:使用 渡渡鸟镜像同步站提供的Node-RED镜像,下载稳定、不易卡顿。

三、Docker部署Node-RED完整步骤
步骤1:拉取Node-RED官方镜像
镜像采用国内华为云镜像源,下载速度更快、稳定性更高,避免官方外网镜像下载超时问题。
执行拉取命令:
|---------------------------------------------------------------------------------------------------|
| Plain Text docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nodered/node-red:latest |

通俗解释:该命令的作用是从国内镜像服务器下载最新稳定版的Node-RED程序镜像,相当于提前下载好安装包,为后续创建运行容器做准备。
常见问题解决:若下载过程卡住、进度停滞,是网络波动或服务器限速导致,并非命令错误。直接终止下载后重新执行命令即可,Docker会自动接续未下载完成的部分,不会重复下载完整镜像。
下载完成后,执行以下命令验证镜像是否保存成功:
|------------------------------------------|
| Plain Text docker images | grep nodered |
若输出nodered相关镜像信息,代表下载成功。

步骤2:创建持久化数据目录(避坑关键)
默认Docker容器删除后,所有配置、插件、程序数据都会丢失。提前创建宿主机专属目录,实现数据持久化,同时解决CentOS 7 SELinux权限报错、settings.js加载失败问题。
- 创建数据存储文件夹:
|---------------------------------------------|
| Plain Text mkdir -p /root/nodered_data/data |

- 开启文件夹全量读写权限,避免权限不足:
|-------------------------------------------------|
| Plain Text chmod -R 777 /root/nodered_data/data |

- 永久关闭SELinux安全模块(CentOS 7专属适配):
|-------------------------------------------------------------------------------|
| Plain Text sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config |

通俗解释:以上操作相当于给Node-RED创建一个"专属存储文件夹",后续所有插件、界面配置、程序代码都保存在这里,就算删除、重建容器,数据也不会丢失;关闭SELinux是为了避免系统安全策略拦截容器读写权限。
步骤3:启动Node-RED基础容器
执行创建并启动容器命令,整合端口映射、数据挂载、用户权限配置:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text docker run -d \ --name node-red \ -p 1880:1880 \ -v /root/nodered_data/data:/data \ --user=root \ swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nodered/node-red:latest |

参数通俗解释:
- -d:让容器在后台静默运行,不占用终端窗口;
- --name node-red:给容器命名为node-red,方便后续操作;
- -p 1880:1880:端口映射,将虚拟机1880端口和容器1880端口绑定,后续通过虚拟机IP访问页面;
- -v:挂载数据目录,实现配置持久化;
- --user=root:以最高权限运行容器,避免插件安装、文件读写报错。
启动后验证容器状态,执行:
|----------------------|
| Plain Text docker ps |

若STATUS显示Up,代表容器运行正常,默认访问地址:http://192.168.227.112:1880。

步骤4:安装核心插件(Dashboard 2.0+串口插件)
本项目使用Dashboard 2.0制作可视化界面,需单独安装插件,同时安装串口插件适配硬件传感器数据传输,插件会安装在持久化目录,无需重复安装。
- 安装Dashboard 2.0可视化插件:
|---------------------------------------------------------------------------------------------|
| Plain Text docker exec -it node-red npm install --prefix /data @flowfuse/node-red-dashboard |

- 安装串口通信插件(对接温湿度、光照等传感器):
|-----------------------------------------------------------------------------------------|
| Plain Text docker exec -it node-red npm install --prefix /data node-red-node-serialport |

通俗解释:docker exec命令是直接对正在运行的容器执行操作,无需进入容器内部,一键安装所需插件,操作更便捷。
步骤5:重启容器使插件生效
插件安装完成后必须重启容器,否则无法加载使用:
|------------------------------------|
| Plain Text docker restart node-red |

重启后打开浏览器访问 http://192.168.227.112:1880,若左侧节点栏出现dashboard、serial分类,代表插件安装成功。


步骤6(可选):挂载USB串口硬件设备
若需要连接真实传感器硬件(非模拟数据),需配置串口映射,让容器识别虚拟机的USB串口设备:
- 验证虚拟机识别串口设备:
|----------------------------|
| Plain Text ls /dev/ttyUSB0 |
有输出内容则代表硬件识别正常。
- 删除旧容器,重建带串口映射的容器:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text docker rm -f node-red docker run -d \ --name node-red \ -p 1880:1880 \ -v /root/nodered_data/data:/data:Z \ --device=/dev/ttyUSB0:/dev/ttyUSB0:rwm \ --user=root \ swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nodered/node-red:latest |

- 验证容器识别串口:
|-----------------------------------------------------|
| Plain Text docker exec -it node-red ls /dev/ttyUSB0 |
输出对应设备地址即挂载成功。
四、Node-RED气象数据界面搭建步骤
步骤1:核心节点选择
本次搭建城市气象数据监测界面,适配四类传感器:温湿度传感器、光照传感器、风速传感器、风向传感器。
选用核心节点:serial in(串口数据接收)、function(数据处理)、template(页面样式/数据展示)、gauge(仪表盘展示)、chart(折线图数据曲线),所有可视化节点均使用Dashboard 2.0自带节点(新旧版dashboard不兼容,仅保留2.0版本)。

步骤2:Dashboard 2.0基础配置
-
打开Node-RED界面,左侧菜单找到【Dashboard2.0】,点击【+ UI】创建新UI项目(自定义名称,如my_Dashboard);
-
在新建UI下创建两个分组:数据显示 (展示各类传感器数值)、系统状态(展示系统时间、页面状态);
-
后续所有展示节点,统一对应绑定两个分组,方便页面分类展示。
步骤3:各节点详细配置
1. serial in****串口节点配置

核心参数:波特率设置为115200,串口选择/dev/ttyUSB0。
通俗说明:该节点是接收硬件传感器传输数据的入口,若找不到该串口,代表虚拟机未连接硬件设备,可直接使用代码模拟数据,不影响界面搭建。
2. function****数据处理节点配置

-
基础设置:节点输出数量改为6路,对应温度、湿度、光照、风速、风向、时间6类数据;
-
代码配置:粘贴文末完整function代码,核心支持双模式切换:
- 模拟模式(USE_SIMULATION = true):无需硬件,自动生成动态波动的气象数据,适合调试界面;
- 真实模式(USE_SIMULATION = false):接收串口硬件传感器的真实数据;
关键注意:function节点输出数据必须为纯数字格式,否则仪表盘、折线图无法正常显示数据。
3. template****界面模板节点配置(三大用途)
该节点是页面美化、数据展示的核心,无需复杂操作,直接粘贴对应代码即可:
- 数据展示模板:绑定【数据显示】分组,粘贴温度、湿度、光照、风速、风向展示代码,自定义数值颜色、字体、布局,直观展示实时数据;

- 时间采集模板:绑定【系统状态】分组,粘贴时钟代码,生成动态走动的模拟时钟和日期文字;


- 全局样式模板:类型选择CSS(all page),无需绑定分组,粘贴全局配色代码,将页面改为科技风蓝黑渐变背景,统一界面色调;


简易避坑:template节点下游无其他节点时,无需勾选"Pass through messages from input",不影响页面显示效果。
4. gauge****仪表盘节点配置
- 样式选择:half Gauge(半圆仪表盘)、style选择needle(指针式);

- 参数设置:自定义尺寸、数值最大值/最小值、物理单位,设置颜色分区区分正常/异常数据;

- 分组绑定:归属【数据显示】分组,用于可视化展示风速等数据。

5. chart****折线图节点配置
- 基础样式:图表类型选择line折线图,曲线模式选择smooth平滑曲线;

- 功能开启:勾选Show Legend显示图例,自定义X/Y轴标签、数值范围;

- 美化设置:自定义网格、曲线、文字颜色,自动适配数据格式,用于展示温湿度、光照数据的动态变化趋势。

五、完整节点代码(直接复制使用)
1. Function****节点完整代码
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text // ============================================== // 模式切换开关(只需要改这一个变量) // ============================================== const USE_SIMULATION = true; // true=使用模拟数据 false=使用真实串口数据 // ============================================== // 全局数据存储 // ============================================== if (typeof context.global.sensorData === 'undefined') { context.global.sensorData = { Temperature: 0, Humidity: 0, Illuminance: 0, Wind_speed: 0, Wind_direction: 0 }; } // 初始化缓存和限流标记 if (typeof context.buffer === 'undefined') { context.buffer = ""; context.currentSensor = null; context.lastSendTime = 0; } // ============================================== // 新增:模拟数据生成模块(真实感动态数据) // ============================================== if (USE_SIMULATION) { // 强制限流:每3秒更新一次(和真实硬件保持一致) const now = Date.now(); if (now - context.lastSendTime < 3000) { return null; } context.lastSendTime = now; // 生成有自然波动的真实感数据 const baseTemp = 25; // 基础温度 const baseHumidity = 60; // 基础湿度 const baseLight = 5000; // 基础光照 const baseWindSpeed = 3; // 基础风速 // 添加随机波动(±20%) const temperature = Math.round((baseTemp + (Math.random() - 0.5) * 10) * 10) / 10; const humidity = Math.round(baseHumidity + (Math.random() - 0.5) * 20); const illuminance = Math.round(baseLight + (Math.random() - 0.5) * 4000); const windSpeed = Math.round((baseWindSpeed + (Math.random() - 0.5) * 4) * 10) / 10; const windDirection = Math.round(Math.random() * 360); // 风向转换(和原有代码完全一致) let windDirText = "北风"; if (windDirection >= 337.5 || windDirection < 22.5) windDirText = "北风"; else if (windDirection >= 22.5 && windDirection < 67.5) windDirText = "东北风"; else if (windDirection >= 67.5 && windDirection < 112.5) windDirText = "东风"; else if (windDirection >= 112.5 && windDirection < 157.5) windDirText = "东南风"; else if (windDirection >= 157.5 && windDirection < 202.5) windDirText = "南风"; else if (windDirection >= 202.5 && windDirection < 247.5) windDirText = "西南风"; else if (windDirection >= 247.5 && windDirection < 292.5) windDirText = "西风"; else if (windDirection >= 292.5 && windDirection < 337.5) windDirText = "西北风"; // 输出格式和原有代码100%一致,不需要修改任何连线 return // 1. 温度数据 { payload: temperature }, // 2. 湿度数据(文本+图表) { payload: humidity, topic: "湿度", timestamp: Date.now() }, // 3. 光照强度数据(文本+图表) { payload: illuminance, topic: "光照强度", timestamp: Date.now() }, // 4. 风速数据(文本+仪表盘) { payload: windSpeed }, // 5. 风向数据(文字) { payload: windDirText }, // 6. 时间数据(保留空输出,兼容原有连线) { payload: "" } ; } // ============================================== // 原有串口数据解析代码(完全保留,当USE_SIMULATION=false时自动生效) // ============================================== if (msg.payload) { // 过滤所有乱码字符 let cleanData = msg.payload.replace(/\^0-9a-zA-Z{}":,.\\-/g, ''); context.buffer += cleanData; // 循环处理所有数据片段 while (true) { // 1. 检测传感器名称 const sensorMatch = context.buffer.match(/"(Temperature|Humidity|Illuminance|Wind_speed|Wind_direction)":{/); if (sensorMatch) { context.currentSensor = sensorMatch1; context.buffer = context.buffer.substring(sensorMatch.index + sensorMatch0.length); continue; } // 2. 检测数值并更新 const valueMatch = context.buffer.match(/"value":(0-9.+)/); if (valueMatch && context.currentSensor) { const value = parseFloat(valueMatch1); context.global.sensorDatacontext.currentSensor = value; context.buffer = context.buffer.substring(valueMatch.index + valueMatch0.length); // 强制限流:每3秒只发送一次最新数据 const now = Date.now(); if (now - context.lastSendTime < 3000) { return null; } context.lastSendTime = now; // 提取所有数据 let temperature = Number(context.global.sensorData.Temperature); let humidity = Number(context.global.sensorData.Humidity); let illuminance = Number(context.global.sensorData.Illuminance); let windSpeed = Number(context.global.sensorData.Wind_speed); let windDirection = Number(context.global.sensorData.Wind_direction); // 风向映射 let windDirText = "北风"; if (windDirection >= 337.5 || windDirection < 22.5) windDirText = "北风"; else if (windDirection >= 22.5 && windDirection < 67.5) windDirText = "东北风"; else if (windDirection >= 67.5 && windDirection < 112.5) windDirText = "东风"; else if (windDirection >= 112.5 && windDirection < 157.5) windDirText = "东南风"; else if (windDirection >= 157.5 && windDirection < 202.5) windDirText = "南风"; else if (windDirection >= 202.5 && windDirection < 247.5) windDirText = "西南风"; else if (windDirection >= 247.5 && windDirection < 292.5) windDirText = "西风"; else if (windDirection >= 292.5 && windDirection < 337.5) windDirText = "西北风"; // Dashboard 2.0 专用输出格式 return // 1. 温度 { payload: temperature }, // 2. 湿度(文本+图表) { payload: humidity, topic: "湿度", timestamp: Date.now() }, // 3. 光照(文本+图表) { payload: illuminance, topic: "光照强度", timestamp: Date.now() }, // 4. 风速(文本+仪表盘) { payload: windSpeed }, // 5. 风向 { payload: windDirText }, // 6. 时间 { payload: "" } ; } // 3. 移除无用的闭合符和逗号 if (context.buffer.startsWith('},') || context.buffer.startsWith('}')) { context.buffer = context.buffer.substring(2); context.currentSensor = null; continue; } break; } // 防止缓存溢出 if (context.buffer.length > 1024) { context.buffer = ""; context.currentSensor = null; } return null; } return null; |
2. Template****节点完整代码
(1)数据显示分组代码(温度/湿度/光照/风速/风向)
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text // 温度展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">温度</div> <div style="font-size:18px; font-weight:500; color:#165DFF;">{{msg.payload}} °C</div> </div> // 湿度展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">湿度</div> <div style="font-size:18px; font-weight:500; color:#165DFF;" translate="no">{{msg.payload}} %RH</div> </div> // 光照强度展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">光照强度</div> <div style="font-size:18px; font-weight:500; color:#165DFF;" translate="no">{{msg.payload}} lux</div> </div> // 风速展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">风速</div> <div style="font-size:18px; font-weight:500; color:#165DFF;" translate="no">{{msg.payload}} m/s</div> </div> // 风向展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">风向</div> <div style="font-size:18px; font-weight:500; color:#165DFF;">{{msg.payload}}</div> </div> |
(2)系统时间采集展示代码
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text <template> <div class="clock-container"> <!-- 左侧模拟时钟 --> <div class="clock"> <div class="clock-face"> <div class="hand hour-hand" :style="{ transform: `rotate({hourDegrees}deg)\` }"\>\