用Node.js打造BeagleBone实时CPU温度监控Web应用
简介面向 BeagleBone 单板计算机的 Node.js 项目核心目标是让用户通过浏览器实时查看 CPU 温度常用于物联网原型开发、硬件远程监控等场景也适合希望将 Node.js 与嵌入式硬件结合的开发者作为学习样例。压缩包约 21.43MB具体文件数量未在下载页列出从项目属性看内容涵盖服务端与前端代码后端基于 Express 搭建 Web 服务借助 WebSocket 与浏览器双向通信持续推送温度数据前端以 JavaScript 展示实时读数和动态图表。通过部署这套源码可以完整掌握 Node.js 读取 BeagleBone 温度数据、配置 WebSocket 服务、在浏览器中实现低延迟监控的流程项目结构清晰适合作为学习传感器数据处理与实时通信的范本读者还能根据需求扩展温度告警、数据记录等功能或迁移到其他单板设备上为后续构建远程运维与可视化项目提供基础。目前已有 65 人学习适合对物联网后端开发感兴趣的开发者参考。 开箱即用总共不到 100 行代码非常适合拿着 BeagleBone 想练手 Node.js 服务端开发的朋友也适合刚入手嵌入式 Linux 板子的新手用来理解“系统数据是怎么一步步变成网页上跳动的数字”的。先交代一下背景。我做这个 AdminApp 的初衷很简单板子放在机柜旁边的角落里常年跑一些脚本偶尔会出现负载飙升、机身发烫的情况。以前想看温度只能 SSH 进去敲命令次数多了就觉得烦于是动了念头——能不能打开浏览器就能看到实时温度于是就有了这个项目。整件事的核心链路并不复杂Linux 内核把温度传感器采集到的数据暴露在 sysfs 接口上Node.js 负责读取并包装成 HTTP 接口浏览器拿到数据后一秒钟刷新一次。听起来很简单但真正去落地的过程中仍然有很多值得注意的细节。下面我把整个项目的设计思路、实现过程、部署方式和踩坑经验都整理出来。1. 项目整体思路与设计决策1.1 数据链路从内核到浏览器先说清楚这台 BeagleBone 上的温度数据是怎么流到浏览器里面的。BeagleBone 系列的处理器比如 AM335x内部自带温度传感器内核启动后会把传感器注册成 thermal zone。在 Linux 系统下温度值会以文件的形式暴露在/sys/class/thermal/目录里。整条链路拆开是三段第一段是“数据源”也就是 sysfs 提供的温度文件。默认情况下读取/sys/class/thermal/thermal_zone0/temp就能拿到原始温度值。注意这个值是毫摄氏度也就是如果要展示成摄氏度必须除以 1000。第二段是“服务端”也就是 AdminApp 里的 Node.js 进程。它负责定时读取这个文件然后包装成一个 JSON 接口比如/api/temp。浏览器端并不能直接读 Linux 文件系统所以中间必须经过这一层 HTTP 转换。第三段是“前端”也就是浏览器里的展示页面。页面每隔一秒向服务端发起一次请求拿到最新的温度值然后通过 DOM 操作更新页面上的数字。这段链路看起来没什么技术含量但有一点是核心设计时要想清楚的到底是谁主动去拿数据这里有两种选择一种是前端轮询也就是 AJAX 定时请求另一种是 WebSocket 或 Server-Sent Events 这种服务端推送方案。我最终选择了前端轮询理由后面会细说。1.2 为什么是 Node.js 而不是 Python提到嵌入式板子上的轻量服务很多人第一反应是 Python 的 Flask 或 FastAPI毕竟 LCTT、OctoPrint 这类知名项目都是 Python 写的。那我为什么偏偏选了 Node.js首先是部署成本。BeagleBone 的官方 Debian 系统里Node.js 通常只需要一条apt install nodejs就能装好而且原生支持 HTTP 服务连框架都不用引。Python 当然也能做但如果用 Flask 的话还要额外 pip 装依赖在嵌入式板子的网络环境下多一个环节就多一个出问题的概率。其次是并发模型。Node.js 的事件循环天然适合这种“IO 密集 小体量”的场景。温度读取本身就是磁盘文件读前端请求就是网络 IO没有复杂的计算逻辑用 Node.js 写起来非常顺手。再者是语言一致性。如果后续想在这个 AdminApp 里加入 Socket.IO 实时推送、或者用 Web 技术做图表可视化前后端都是 JavaScript思路会非常连贯。我后面确实用到了这个优势稍后会提到。关于“轮询还是长连接”的问题我实际测试下来单机单用户场景下每秒一次的轮询请求对 BeagleBone 的压力可以忽略不计。而 WebSocket 方案会增加前端和服务端的复杂度在没有任何并发压力的情况下属于过度设计。先把简单方案跑通再把数据喂给可视化工具这条路最顺。2. 环境准备与温度数据源2.1 系统和基础配置我手里这块是 BeagleBone Black Rev C板载 Debian 系统。如果你用的不是官方系统也没关系只要是 Linux 发行版下面这套逻辑基本都通用。第一步是确认板子联网和基本系统信息。SSH 登录后先跑一条命令看一下系统版本cat /etc/os-release然后看一下内核版本uname -a这两条命令主要是为了确认系统的包管理器和内核是否正常。接下来安装 Node.js。BeagleBone 官方 Debian 系统里带的 Node.js 版本有时候比较旧但用来跑这个项目完全足够。如果你希望用比较新的 LTS 版本可以从 NodeSource 仓库安装不过我实测淘宝镜像源的配置在板子上有时候不太稳定这里直接推荐一条最稳妥的方式sudo apt update sudo apt install -y nodejs npm装完之后验证一下node -v npm -v我装完之后 Node 版本是 v10.x 左右虽然老但跑这种小型服务没有任何问题。如果你的板子系统比较精简连 node 都装不上可以考虑用二进制包直接解压到/usr/local这种方式的兼容性也比较好。2.2 CPU 温度到底存在哪里在写代码之前最关键的一步是搞清楚温度数据在系统的哪个位置。先登录板子执行ls /sys/class/thermal/正常会看到thermal_zone0、thermal_zone1这样的目录每个目录代表一个温度区域。BeagleBone Black 上通常thermal_zone0就是 SoC 内部温度传感器。进去看一下里面的文件ls /sys/class/thermal/thermal_zone0/重点关注两个文件temp和type。type告诉这个温度区是什么类型temp就是当前温度。读取一下当前温度cat /sys/class/thermal/thermal_zone0/temp输出大概是48210这样的数字。注意这里的单位是毫摄氏度所以实际温度是48.2摄氏度。这里有个很容易踩坑的点不同版本的内核和不同板型thermal_zone 的编号可能不一样。比如有些板子thermal_zone1才是 CPU 温度。保险起见先把所有 thermal_zone 下面的type文件都 cat 一遍确认哪一个是真正的 CPU 温度源再写进代码里。2.3 Node.js 环境验证安装完 Node.js 后先写个小脚本验证一下能否正常读取温度。在项目目录下建一个test.jsconst fs require(fs); const data fs.readFileSync(/sys/class/thermal/thermal_zone0/temp, utf8); const temp (parseInt(data.trim(), 10) / 1000).toFixed(1); console.log(当前 CPU 温度: ${temp}℃);然后运行node test.js如果输出正常说明数据链路的第一步已经打通。接下来可以正式写 AdminApp 的服务端代码了。3. 核心代码实现3.1 服务端读取温度并输出 JSONAdminApp 的核心服务端是一个原生 Node.js HTTP 服务没有用 Express因为真的不需要。用原生http模块代码更少依赖更少跑在板子上也更省内存。先明确两个接口/返回一个 HTML 页面供浏览器访问/api/temp返回 JSON 格式的温度数据/api/temp的处理逻辑很简单读取温度文件把毫摄氏度转成摄氏度加上当前时间戳以 JSON 格式返回。这里有一个设计上的细节是否要加上缓存如果前端每秒请求一次每次请求都去读一次文件系统虽然对嵌入式 Linux 来说也是毛毛雨但还是可以做一点优化。我在代码里加了一个 1 秒的缓存也就是在同一秒内的多次请求直接返回上一次的结果避免重复读文件。这种小优化在低配设备上积少成多还是值得做的。下面贴出服务端代码const http require(http); const fs require(fs); const TEMP_PATH /sys/class/thermal/thermal_zone0/temp; const PORT 8080; let cachedData null; let cacheTime 0; function getCpuTemp() { const now Date.now(); if (cachedData now - cacheTime 1000) { return cachedData; } try { const raw fs.readFileSync(TEMP_PATH, utf8).trim(); const temp (parseInt(raw, 10) / 1000).toFixed(1); cachedData { temp: temp, unit: C, time: now, }; cacheTime now; } catch (e) { cachedData { temp: null, unit: C, time: now, error: e.message, }; cacheTime now; } return cachedData; } http.createServer((req, res) { if (req.url /api/temp) { const data getCpuTemp(); res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify(data)); return; } if (req.url /) { res.writeHead(200, { Content-Type: text/html; charsetutf-8 }); res.end(getHtmlPage()); return; } res.writeHead(404, { Content-Type: text/plain }); res.end(Not Found); }).listen(PORT, 0.0.0.0, () { console.log(AdminApp running at http://0.0.0.0:${PORT}); });这段代码里面有个容易忽视的坑监听地址必须写0.0.0.0不能只写127.0.0.1或者localhost。否则的话你在同一局域网的其他设备上访问不到这个页面。我一开始在这上面吃过亏只监听了回环地址结果手机怎么都打不开网页排查了半天才发现是监听地址的问题。3.2 前端浏览器里的实时刷新前端部分我直接在服务端代码里内嵌了一个getHtmlPage()函数返回一个完整的 HTML 页面。不是特别复杂的页面就没有单独建静态目录这样整个项目就一个文件部署起来特别省事。页面主要分两块顶部显示当前温度数字下面用一段简短的文字说明状态。为了让温度值直观反映“热不热”我根据温度范围给数字加上了不同的颜色——低于 55℃ 绿色55℃ 到 70℃ 黄色超过 70℃ 红色。这个配色逻辑可以帮你快速扫一眼页面就能判断板子是否过热。前端刷新用的是setInterval加fetch逻辑很简单async function fetchTemp() { try { const res await fetch(/api/temp); const data await res.json(); if (data.temp ! null) { const temp parseFloat(data.temp); const el document.getElementById(temp); el.textContent data.temp ℃; el.className temp 70 ? hot : temp 55 ? warm : cool; } } catch (e) { console.error(Failed to fetch temperature, e); } } setInterval(fetchTemp, 1000); fetchTemp();用setInterval而不是setTimeout嵌套在请求很快、间隔较长的场景下差别不大。但如果你把刷新频率提到 200ms 以内就要留意请求返回时间和定时器重叠可能导致的页面卡顿。作为温度监控这样的低频场景1 秒一次是完全够用的。3.3 完整代码演示把服务端、前端、样式合并成一个server.js完整的代码如下所示。这段代码直接复制到 BeagleBone 上保存为文件然后node server.js就能跑起来。const http require(http); const fs require(fs); const TEMP_PATH /sys/class/thermal/thermal_zone0/temp; const PORT 8080; let cachedData null; let cacheTime 0; function getCpuTemp() { const now Date.now(); if (cachedData now - cacheTime 1000) { return cachedData; } try { const raw fs.readFileSync(TEMP_PATH, utf8).trim(); const temp (parseInt(raw, 10) / 1000).toFixed(1); cachedData { temp, unit: C, time: now }; } catch (e) { cachedData { temp: null, unit: C, time: now, error: e.message }; } cacheTime now; return cachedData; } function getHtmlPage() { return !DOCTYPE html html langzh-CN head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 titleAdminApp - CPU 温度监控/title style body { font-family: sans-serif; background: #1a1a1a; color: #fff; text-align: center; padding-top: 10vh; } #temp { font-size: 5rem; font-weight: bold; transition: color 0.3s; } .cool { color: #00c853; } .warm { color: #ffd600; } .hot { color: #d50000; } .info { margin-top: 20px; color: #aaa; } /style /head body h1BeagleBone CPU 温度监控/h1 div idtemp-- ℃/div div classinfo页面每 1 秒自动刷新一次/div script async function fetchTemp() { try { const res await fetch(/api/temp); const data await res.json(); if (data.temp ! null) { const t parseFloat(data.temp); const el document.getElementById(temp); el.textContent data.temp ℃; el.className t 70 ? hot : t 55 ? warm : cool; } } catch (e) { console.error(fetch error, e); } } setInterval(fetchTemp, 1000); fetchTemp(); /script /body /html; } http.createServer((req, res) { if (req.url /api/temp) { const data getCpuTemp(); res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify(data)); return; } if (req.url /) { res.writeHead(200, { Content-Type: text/html; charsetutf-8 }); res.end(getHtmlPage()); return; } res.writeHead(404, { Content-Type: text/plain }); res.end(Not Found); }).listen(PORT, 0.0.0.0, () { console.log(AdminApp running at http://0.0.0.0: PORT); });保存文件后在这个文件所在目录执行node server.js然后在同一局域网内用任意现代浏览器访问http://板子的IP:8080就能看到实时温度页面了。访问http://板子的IP:8080/api/temp还能直接看到 JSON 数据输出。4. 部署、自启动与问题排查4.1 systemd 服务配置本地手动执行node server.js只是临时运行一旦 SSH 断开或者板子重启服务就没了。To keep AdminApp running reliably, 我用 systemd 把服务托管起来。先创建一个 systemd service 文件sudo nano /etc/systemd/system/adminapp.service内容如下[Unit] DescriptionAdminApp CPU Temperature Monitor Afternetwork.target [Service] WorkingDirectory/opt/adminapp ExecStart/usr/bin/node server.js Restartalways RestartSec3 [Install] WantedBymulti-user.target这里有两处要注意第一ExecStart里的 node 路径要写绝对路径可以通过which node查看第二Restartalways很关键就算 Node.js 进程因为异常退出了systemd 也会在 3 秒后重新拉起。配置好之后执行以下命令sudo systemctl daemon-reload sudo systemctl enable adminapp sudo systemctl start adminapp然后查看运行状态sudo systemctl status adminapp看到active (running)就说明成功了。如果没起来可以用下面的命令查看日志journalctl -u adminapp -f这个命令会实时输出服务日志排查错误时非常好用。4.2 典型问题排查速查表我在实际使用和给朋友远程排错的过程中总结了几个非常常见的坑列成一张表方便你对照排查。现象可能原因排查方法浏览器打不开页面服务没启动、防火墙拦了、IP 错了板子上执行curl localhost:8080如果能通说明服务正常那就检查 IP 和防火墙页面正常但温度一直不变温度文件路径写死、或者权限不足在板子上cat /sys/class/thermal/thermal_zone0/temp看是否有输出JSON 返回temp: nullNode 进程读取文件失败通常是路径或权限问题查看服务日志、确认运行用户是否有读取 sysfs 的权限板子重启后服务消失systemd 配置没设 enable执行systemctl enable adminapp并确认服务文件在/etc/systemd/system/温度显示异常高/异常低thermal_zone 选错用cat /sys/class/thermal/thermal_zone*/type确认所选 zone 是 CPU 传感器还有一个很容易被忽略的点如果你改了server.js里的代码一定记得重启 systemd 服务让改动生效systemctl restart adminapp这条命令要多用。4.3 实测数据与性能表现最后分享一个我很在意的指标这个服务在 BeagleBone Black 上到底吃了多少资源。实测下来Node.js 进程占用内存大约 30MB 左右CPU 占用率基本在 1% 上下浮动。对于一块 512MB 内存的板子来说这个开销是非常轻量的。我还试过把刷新频率提高到 200ms也就是每秒请求 5 次CPU 占用大概上升到 2%~3% 左右依然非常轻松。当然这种程度的监控数据也没有必要那么高频。温度数据验证方面我用stress工具给 CPU 加负载测试了一下。执行stress --cpu 4 --timeout 120模拟高负载场景大概 1 分钟后页面上的温度就从待机状态的 48℃ 左右缓慢爬升到 65℃颜色也从绿色变成了黄色再停掉压力测试后温度逐渐回落到正常值。整个过程页面数据跟随非常及时没有出现丢帧或者卡顿。这说明只要数据源正确、接口逻辑简洁Web 端实时监控嵌入式板卡的硬件指标是完全可行的。这也算是给后续扩展做了个验证。4.4 几个可以继续玩的方向如果你觉得只显示温度还不够满足需求AdminApp 的扩展空间非常充足。我这里列三个我实际验证过或者正在做方向供你参考。一是增加更多传感器数据。BeagleBone 系列板子自带不少 ADC 引脚可以外接温湿度传感器在 Node.js 里统一读取后前端一并展示。这样监控系统就不只是 CPU 温度还能覆盖环境温湿度。二是做成可视化图表。在页面里接入一个简单的 Canvas 绘图逻辑把每次采样的温度绘制成折线图。如果你不想自己画也可以直接让 AdminApp 把 JSON 数据推给 Grafana从“单一监控页”升级成“可回溯分析系统”。三是放进更多板子做成小集群监控。BeagleBone 和树莓派各自的 AdminApp 独立运行然后在上层用一个聚合服务定时拉取所有节点的/api/temp接口。这个思路我目前正在做感觉比一张张打开网页再切标签页爽得多。关于这个项目我个人的体会是它虽然代码量不大但把 Linux 文件系统、HTTP 服务、前端交互、进程守护这几块硬知识串在了一起。对初学者来说是一个非常完整的全栈微型案例对老手来说也是一个能快速改造成自己工具的模板。如果你也想给自己的嵌入式板子添一个这样的小功能照着上面的步骤操作半小时左右就能跑起来。遇到问题的话优先看日志 —journalctl -u adminapp -f和curl localhost:8080/api/temp这两条命令能解决八成以上的郁闷。本文还有配套的精品资源点击获取