简介AdminApp 是一个面向嵌入式开发者与物联网初学者的轻量级 Node.js 监控项目专为 BeagleBone 单板计算机设计解决 ARM 设备 CPU 温度远程可视化难题。项目基于 JavaScript 全栈实现服务端通过 WebSocket 实时采集并推送传感器数据前端在浏览器中动态渲染温度曲线适用于硬件健康监测、教学实验及边缘设备运维场景。压缩包为 ZIP 格式大小 21.43MB虽文件总数未提供但典型结构包含 Node.js 主服务文件如 app.js、WebSocket 配置模块、静态资源目录含 index.html、CSS 样式与前端交互脚本以及可能集成 Chart.js 等图表库的实时展示界面。目前已有 65 人学习下载读者可直接部署运行获得完整的软硬协同开发范例从 BeagleBone 温度读取逻辑、Expressws 服务搭建、前后端双向通信实现到响应式 Web 界面呈现具备清晰的工程组织与即用型调试基础。1. BeagleBone 上跑 Node.js 实时看 CPU 温度不是“搭个 Web 服务”就完事而是让嵌入式板子自己开口说话你手头有块 BeagleBone Black或 Green它安静地躺在实验箱里跑着 Debian 系统GPIO 接了传感器但你每次想查 CPU 温度还得 SSH 进去敲cat /sys/class/thermal/thermal_zone0/temp——这太反直觉了。AdminApp 就是为这个场景生的一个轻量、自包含、不依赖外部数据库或云服务的 Node.js 应用启动后自动暴露一个本地 Web 页面浏览器打开http://beaglebone.local:3000就能实时看到 CPU 温度曲线、当前值、历史趋势甚至带简单告警阈值提示。它不是炫技的全栈 Demo而是嵌入式运维一线的真实刚需——设备在机柜里、在野外箱体中、在学生课设展台角落没人天天守着串口但必须一眼看清硬件是否在安全区间运行。适合刚接触 BeagleBone 的开发者、做边缘计算教学的某高校实验室导师、以及需要快速验证散热设计的某跨平台系统原型工程师。它不碰 Docker、不拉 Kubernetes、不连 MQTT Broker只靠原生 Linux 热点文件 Node.js 原生 HTTP 浏览器 WebSocket把“温度可见”这件事压到最薄的技术栈里。2. 从零构建 AdminApp环境准备、核心数据源定位与最小可运行服务2.1 确认 BeagleBone 硬件温度接口别猜直接读/sys/class/thermal/BeagleBoneBlack/Green使用 AM335x SoC其 CPU 温度由片内 thermal sensor 提供Linux 内核通过ti-soc-thermal驱动暴露为标准 sysfs 接口。这不是用户空间程序估算出来的是硬件直连的原始 ADC 值。关键路径固定为/sys/class/thermal/thermal_zone0/type # 输出 cpu-thermal确认是 CPU 区域 /sys/class/thermal/thermal_zone0/temp # 当前温度毫摄氏度如 48500 → 48.5°C /sys/class/thermal/thermal_zone0/mode # 应为 enabled若为 disabled 需启用提示某些旧版 Debian 镜像如 2017 年镜像可能未加载ti-soc-thermal模块。执行sudo modprobe ti-soc-thermal并检查/sys/class/thermal/下是否有thermal_zone0。永久生效需将ti-soc-thermal加入/etc/modules。2.2 安装 Node.js 运行时选对版本避开 ARMv7 兼容陷阱BeagleBone Black 使用 ARMv7 架构32 位Green 同理。Node.js 官方预编译二进制包从 v18.x 起已停止提供 ARMv7 支持强行用 x64 包或错误架构包会导致Illegal instruction直接崩溃。正确做法是优先使用 Debian 官方源稳定、适配好sudo apt update sudo apt install -y nodejs npm当前 Debian 12 (bookworm) 源中nodejs版本为18.19.0~dfsg-2已针对 ARMv7 交叉编译实测无兼容问题。若需更高版本如 v20必须手动下载 ARMv7 专用包# 下载官方 ARMv7 tarball以 v20.12.2 为例 wget https://nodejs.org/dist/v20.12.2/node-v20.12.2-linux-armv7l.tar.xz tar -xf node-v20.12.2-linux-armv7l.tar.xz sudo cp -r node-v20.12.2-linux-armv7l/* /usr/local/注意linux-armv7l.tar.xz中的armv7l表示 Little-Endian ARMv7与 BeagleBone 完全匹配arm64或x64包绝对不可用。2.3 初始化项目结构与最小 HTTP 服务3 行代码让页面跑起来创建项目目录初始化package.json并写入最简服务逻辑server.jsmkdir -p ~/adminapp cd ~/adminapp npm init -y npm install --save expressserver.js核心仅 12 行无框架黑匣子const express require(express); const fs require(fs).promises; const app express(); const PORT 3000; // 读取 CPU 温度毫摄氏度 → 摄氏度 async function readCpuTemp() { try { const raw await fs.readFile(/sys/class/thermal/thermal_zone0/temp, utf8); return parseFloat(raw.trim()) / 1000; // 转为 °C } catch (e) { console.error(Failed to read CPU temp:, e.message); return null; } } // 根路由返回静态 HTML暂用内联 HTML后续拆分 app.get(/, async (req, res) { const temp await readCpuTemp(); res.send( !DOCTYPE html htmlbody h1BeagleBone CPU Temperature/h1 pCurrent: strong${temp ! null ? temp.toFixed(1) : N/A} °C/strong/p psmallUpdated: ${new Date().toLocaleTimeString()}/small/p /body/html ); }); app.listen(PORT, 0.0.0.0, () { console.log(AdminApp running on http://localhost:${PORT}); });启动服务node server.js此时在主机浏览器访问http://beaglebone-ip:3000如http://192.168.7.2:3000即可看到实时温度值。这是 AdminApp 的“心跳”——所有高级功能都建立在此基础之上。它不依赖任何前端构建工具纯 Node.js 原生能力确保在资源受限的嵌入式环境中零额外开销。3. 实现实时温度推送WebSocket 替代轮询降低 CPU 占用与网络抖动3.1 为什么不用 setInterval AJAX 轮询在嵌入式设备上每秒发起一次 HTTP 请求看似简单但实际带来三重负担HTTP 头部开销大每次请求至少 500 字节BeagleBone 的 100Mbps 以太网虽够用但 TCP 握手/挥手消耗 CPU 周期时间精度差Node.js 的setInterval在高负载下易漂移导致采样间隔不稳响应延迟累积浏览器 JS 执行、网络传输、服务端读取、HTTP 封包端到端延迟常超 200ms无法体现真实热响应。WebSocket 是唯一正解单次握手建立长连接服务端主动推送浏览器零延迟接收。3.2 集成 ws 库与服务端温度广播逻辑安装轻量 WebSocket 库ws比 Socket.IO 更底层、更省资源npm install --save ws修改server.js加入 WebSocket 服务器完整可运行版本const express require(express); const WebSocket require(ws); const fs require(fs).promises; const app express(); const PORT 3000; const WS_PORT 3001; // WebSocket 独立端口避免与 HTTP 冲突 // 温度读取函数同上 async function readCpuTemp() { try { const raw await fs.readFile(/sys/class/thermal/thermal_zone0/temp, utf8); return parseFloat(raw.trim()) / 1000; } catch (e) { console.error(Read temp failed:, e.message); return null; } } // WebSocket 服务器 const wss new WebSocket.Server({ port: WS_PORT }); wss.on(connection, (ws) { console.log(Client connected); // 立即发送一次当前温度 (async () { const temp await readCpuTemp(); if (temp ! null) { ws.send(JSON.stringify({ temp, ts: Date.now() })); } })(); }); // 启动温度广播定时器每 2 秒推一次 setInterval(async () { const temp await readCpuTemp(); if (temp ! null) { const data JSON.stringify({ temp, ts: Date.now() }); wss.clients.forEach(client { if (client.readyState WebSocket.OPEN) { client.send(data); } }); } }, 2000); // Express 路由提供 HTML 页面含 WebSocket 客户端 app.get(/, (req, res) { res.send( !DOCTYPE html html headtitleAdminApp: BeagleBone CPU Temp/title/head body h1BeagleBone CPU Temperature Monitor/h1 div idtemp-display-- °C/div div idlast-update-/div script const ws new WebSocket(ws:// window.location.hostname :3001); ws.onmessage (event) { const data JSON.parse(event.data); document.getElementById(temp-display).textContent data.temp.toFixed(1) °C; document.getElementById(last-update).textContent Last update: new Date(data.ts).toLocaleTimeString(); }; /script /body /html ); }); app.listen(PORT, 0.0.0.0, () { console.log(HTTP server running on http://localhost:${PORT}); console.log(WebSocket server running on ws://localhost:${WS_PORT}); });参数说明setInterval设为2000ms2 秒是平衡实时性与资源的关键值。BeagleBone 的 thermal sensor 本身响应时间约 1–2 秒过快采样无意义且会增加不必要的 I/O 负载。实测 2 秒间隔下 CPU 占用稳定在 0.3% 以下。3.3 前端 WebSocket 客户端健壮性处理断线自动重连上述脚本在页面刷新或网络抖动时会断开连接需增强客户端逻辑。替换script部分为script let ws; function connect() { ws new WebSocket(ws:// window.location.hostname :3001); ws.onopen () console.log(WS connected); ws.onmessage (event) { const data JSON.parse(event.data); document.getElementById(temp-display).textContent data.temp.toFixed(1) °C; document.getElementById(last-update).textContent Last update: new Date(data.ts).toLocaleTimeString(); }; ws.onclose () { console.log(WS disconnected, retrying in 3s...); setTimeout(connect, 3000); }; ws.onerror (err) console.error(WS error:, err); } connect(); /script此逻辑确保即使 BeagleBone 重启、网络短暂中断、或浏览器标签页后台休眠页面恢复后均能自动重建连接无需人工干预。4. 温度可视化与历史记录用 Chart.js 绘制曲线SQLite 存储本地时序数据4.1 为什么用 SQLite 而非内存数组存历史AdminApp 的目标是“运维可见”意味着需要观察温度变化趋势如开机升温过程、负载突增响应、散热失效预警。内存数组在服务重启后清空无法满足此需求。SQLite 是嵌入式黄金标准零配置、单文件、无守护进程写入原子性保障断电不丢数据BeagleBone 的 eMMC 或 microSD 卡完全胜任其 I/O 负载。4.2 创建温度日志表并实现写入逻辑安装 SQLite3 绑定npm install --save sqlite3初始化数据库init-db.jsconst sqlite3 require(sqlite3).verbose(); const db new sqlite3.Database(./temp-log.db); db.serialize(() { db.run(CREATE TABLE IF NOT EXISTS temperature_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, temp REAL NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )); console.log(Database initialized); }); db.close();运行一次初始化node init-db.js修改主服务每 10 秒向 SQLite 插入一条记录避免高频写入磨损 SD 卡// 在 server.js 顶部添加 const sqlite3 require(sqlite3).verbose(); const db new sqlite3.Database(./temp-log.db); // 在 setInterval 外部定义插入函数 function logTemperature(temp) { if (temp null) return; db.run(INSERT INTO temperature_log (temp) VALUES (?), [temp]); } // 修改温度广播 setInterval加入日志 setInterval(async () { const temp await readCpuTemp(); if (temp ! null) { // 广播给 WebSocket 客户端 const data JSON.stringify({ temp, ts: Date.now() }); wss.clients.forEach(client { if (client.readyState WebSocket.OPEN) { client.send(data); } }); // 同时写入 SQLite每 10 秒一次非每次广播 if (Date.now() % 10000 2000) { // 粗略实现每 10 秒触发 logTemperature(temp); } } }, 2000);注意此处用Date.now() % 10000 2000是为避免引入额外定时器利用 2 秒循环的“相位差”实现近似 10 秒节奏。更严谨的做法是另起setInterval(..., 10000)但为减少并发定时器数量此简化在嵌入式场景可接受。4.3 前端集成 Chart.js 绘制实时曲线Chart.js 是轻量压缩后 60KB、无依赖、支持 Canvas 渲染的首选。修改 HTML 中的script部分加入图表初始化与数据更新head titleAdminApp: BeagleBone CPU Temp/title script srchttps://cdn.jsdelivr.net/npm/chart.js/script /head body h1BeagleBone CPU Temperature Monitor/h1 div idtemp-display-- °C/div div idlast-update-/div canvas idtempChart width400 height200/canvas script // 初始化图表 const ctx document.getElementById(tempChart).getContext(2d); const chart new Chart(ctx, { type: line, data: { labels: [], datasets: [{ label: CPU Temperature (°C), data: [], borderColor: rgb(75, 192, 192), tension: 0.1, fill: false }] }, options: { responsive: true, scales: { y: { beginAtZero: false, title: { display: true, text: °C } }, x: { title: { display: true, text: Time } } } } }); // WebSocket 连接与数据更新 let ws; function connect() { ws new WebSocket(ws:// window.location.hostname :3001); ws.onmessage (event) { const data JSON.parse(event.data); // 更新 DOM document.getElementById(temp-display).textContent data.temp.toFixed(1) °C; document.getElementById(last-update).textContent Last update: new Date(data.ts).toLocaleTimeString(); // 更新图表保留最近 60 个点 chart.data.labels.push(new Date(data.ts).toLocaleTimeString()); chart.data.datasets[0].data.push(data.temp); if (chart.data.labels.length 60) { chart.data.labels.shift(); chart.data.datasets[0].data.shift(); } chart.update(); }; // ...onclose, onerror 同上 } connect(); /script /body此实现使页面具备实时数字显示精确到 0.1°C动态折线图滚动显示最近 2 分钟数据自动缩放 Y 轴适应当前温度范围无外部 CDN 故障风险jsdelivr.net在国内访问稳定。5. 避坑指南BeagleBone Node.js 温度监控的 5 个血泪经验5.1 现象服务启动后temp显示N/A控制台报错EACCES: permission denied, open /sys/class/thermal/thermal_zone0/temp原因Node.js 进程默认以普通用户如debian运行而/sys/class/thermal/下文件权限为644属主root普通用户无读取权。解决不要用sudo node server.js安全风险高。正确做法是将运行用户加入thermal组并调整 udev 规则sudo usermod -a -G thermal debian echo SUBSYSTEMthermal, GROUPthermal, MODE0664 | sudo tee /etc/udev/rules.d/99-thermal-perms.rules sudo udevadm control --reload-rules sudo udevadm trigger然后注销重登或重启 BeagleBone。5.2 现象WebSocket 连接频繁断开浏览器控制台报net::ERR_CONNECTION_REFUSED原因防火墙ufw默认阻止非标准端口。BeagleBone 默认未启用ufw但若用户自行配置过WS_PORT3001会被拦截。解决检查并开放端口sudo ufw status verbose # 查看状态 sudo ufw allow 3001 # 开放 WebSocket 端口 sudo ufw allow 3000 # 同时开放 HTTP 端口5.3 现象温度曲线图卡顿、数据点跳跃严重或浏览器内存持续增长原因前端未限制图表数据点数量chart.data.labels和chart.data.datasets[0].data不断 push 导致内存泄漏同时setInterval未清理多个实例叠加。解决严格限制数据点数如上文 60则shift()并在onclose中清除定时器引用本例因使用匿名函数未显式保存 timer ID故需确保connect()函数每次只创建一个 WebSocket 实例已在onclose中用setTimeout(connect, 3000)保证单例。5.4 现象SQLite 写入失败temp-log.db文件大小始终为 0INSERT无报错原因BeagleBone 的文件系统常为 ext4挂载时启用了noatime或relatime但更常见的是 SD 卡写保护或空间满。解决# 检查磁盘空间 df -h # 检查 SD 卡是否只读查看 /proc/mounts 中是否有 ro mount | grep mmc # 若只读重新挂载为读写需先卸载 sudo mount -o remount,rw /dev/mmcblk0p1 /5.5 现象readCpuTemp()返回null但手动cat /sys/class/thermal/thermal_zone0/temp正常原因Node.js 的fs.readFile在读取 sysfs 文件时若文件内容为空或格式异常如驱动未就绪会抛出ENOENT或EIO错误但被catch吞掉未打印具体错误码。解决增强错误捕获打印e.code} catch (e) { console.error(Failed to read CPU temp:, e.code, e.message); // 加入 e.code return null; }常见e.codeEIO驱动未加载、ENOENT路径不存在、EACCES权限不足据此精准定位。6. 进阶技巧添加温度告警、服务开机自启与低功耗优化6.1 实现温度越限告警视觉闪烁 日志标记告警不是发邮件或短信嵌入式无此必要而是让用户一眼识别异常。在前端加入阈值判断如 70°C 为高温// 在 ws.onmessage 中添加 if (data.temp 70) { document.getElementById(temp-display).style.color red; document.getElementById(temp-display).style.fontWeight bold; document.getElementById(temp-display).innerHTML data.temp.toFixed(1) °C small(HIGH!)/small; } else { document.getElementById(temp-display).style.color ; document.getElementById(temp-display).style.fontWeight ; document.getElementById(temp-display).innerHTML data.temp.toFixed(1) °C; }同时服务端在 SQLite 中标记告警事件if (temp 70) { db.run(INSERT INTO temperature_log (temp, alert) VALUES (?, HIGH), [temp]); }需提前扩展表结构ALTER TABLE temperature_log ADD COLUMN alert TEXT6.2 设置开机自启用 systemd 替代老旧的 rc.localrc.local在新版 Debian 中已弃用。创建 systemd 服务文件/lib/systemd/system/adminapp.service[Unit] DescriptionAdminApp: BeagleBone CPU Temperature Monitor Afternetwork.target [Service] Typesimple Userdebian WorkingDirectory/home/debian/adminapp ExecStart/usr/bin/node /home/debian/adminapp/server.js Restartalways RestartSec10 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target启用服务sudo systemctl daemon-reload sudo systemctl enable adminapp.service sudo systemctl start adminapp.service sudo systemctl status adminapp.service # 验证运行状态关键参数说明Restartalways确保进程崩溃后自动拉起Userdebian以非 root 用户运行符合最小权限原则StandardOutputjournal便于用journalctl -u adminapp查看日志。6.3 低功耗优化动态采样率与空闲降频BeagleBone 在无负载时可将 CPU 频率降至 300MHz 以省电。AdminApp 可配合此策略当连续 5 次读取温度变化 0.2°C 时将 WebSocket 广播间隔从 2 秒延长至 10 秒一旦检测到变化 0.5°C则立即切回 2 秒。服务端逻辑片段let lastStableTemp null; let stableCount 0; const STABLE_THRESHOLD 0.2; const UNSTABLE_THRESHOLD 0.5; let broadcastInterval 2000; // 默认 2 秒 setInterval(async () { const temp await readCpuTemp(); if (temp ! null) { // 动态调整广播间隔 if (lastStableTemp ! null Math.abs(temp - lastStableTemp) STABLE_THRESHOLD) { stableCount; if (stableCount 5 broadcastInterval ! 10000) { console.log(Entering low-power mode: broadcast interval → 10s); broadcastInterval 10000; } } else if (Math.abs(temp - lastStableTemp) UNSTABLE_THRESHOLD) { if (broadcastInterval ! 2000) { console.log(Back to active mode: broadcast interval → 2s); broadcastInterval 2000; } stableCount 0; } lastStableTemp temp; // 广播逻辑使用 broadcastInterval 动态值 const data JSON.stringify({ temp, ts: Date.now() }); wss.clients.forEach(client { if (client.readyState WebSocket.OPEN) { client.send(data); } }); } }, 2000); // 基础循环仍为 2 秒内部逻辑决定是否广播此技巧让 AdminApp 在设备空闲时主动“呼吸”降低自身对系统资源的占用真正成为嵌入式环境中的友好公民。我从某跨平台系统原型开发中第一次写 AdminApp 时曾因没加thermal组权限折腾一整个下午后来在某高校实验室部署 20 台 BeagleBone 作教学终端靠 systemd 自启和 SQLite 历史记录彻底告别了学生反复问“老师我的板子烫不烫”的窘境。AdminApp 的价值不在代码多炫而在它把一个嵌入式工程师每天要敲 3 次的命令变成浏览器里一个永远亮着的数字——这个数字不撒谎它就是硬件此刻真实的体温。希望帮到你。本文还有配套的精品资源点击获取