好想给自己的网页加点功能

环境准备

考虑到树莓派对 python 的支持良好,且 python 的各种包实在是非常好用,这里用 python 的 Flask 框架作为网页后端

树莓派 raspberryPi OS 中的 python 环境作为系统服务,是不允许直接使用 pip 进行包管理的,这里我们使用 Debian 维护的,非常好用apt 包管理器来安装需要的 python 包

有良好习惯的同学们可能有疑问,为什么不使用 python 进行虚拟环境的隔离呢?因为虚拟环境中无法直接调用树莓派有关系统环境的 API,需要使用python -m venv --system-site-packages <虚拟环境路径>参数在虚拟环境中调用全局系统包,那我为什么不直接用全局环境呢?我暂时没有发现比较好的办法

我们使用以下命令下载需要的包

# 更新系统包列表
sudo apt update

# 安装所需的 Python 包
sudo apt install python3-flask python3-psutil python3-flask-cors

这里,pyhton3-flaskpython-flask-cors分别是 Flask 框架和对应的跨域服务,python3-psutil则是调用系统状态的 API 所必要的包

服务器实现

在你喜欢的地方,写一个后端提供系统数据的 API,这里我在 /home/kpmark/nav/status.py中进行编写

import psutil
from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(name)
# 允许特定域名的跨域请求
CORS(app, resources={
    r"/status": {
        "origins": ["https://www-blog.u2330056.nyat.app:49191"],
        "methods": ["GET"],
        "allow_headers": ["Content-Type"]
    }
})

@app.route('/status')
def get_status():
    cpu_percent = psutil.cpu_percent(interval=1)
    memory = psutil.virtual_memory()
    disk = psutil.disk_usage('/')
    temperatures = psutil.sensors_temperatures()

    # 获取CPU温度(针对树莓派)
    cpu_temp = None
    if 'cpu_thermal' in temperatures:
        cpu_temp = temperatures['cpu_thermal'][0].current

    return jsonify({
        'cpu_percent': cpu_percent,
        'memory_percent': memory.percent,
        'disk_percent': disk.percent,
        'cpu_temp': cpu_temp,
        'memory_total': memory.total,
        'memory_used': memory.used,
        'disk_total': disk.total,
        'disk_used': disk.used
    })

if name == 'main':
    app.run(host='127.0.0.1', port=3001)

这样,就有一个后端可以挂在你树莓派的 3001 端口了,但是我们想让他自动在后台启动,需要配置相应的系统服务

配置系统服务

这里,使用以下命令创建 systemd 服务文件

sudo cd /etc/systemd/system/
sudo touch raspstatus.service

然后填入以下内容

[Unit]
Description=Raspberry Pi Status API
After=network.target

[Service]
User=kpmark
WorkingDirectory=/home/kpmark/nav
ExecStart=/usr/bin/python3 /home/kpmark/nav/status.py
Restart=always

[Install]
WantedBy=multi-user.target
  • Description:描述服务的名称和用途,这里是 “Raspberry Pi Status API”
  • After:指定服务启动的依赖顺序,表示该服务将在 network.target(网络服务)启动后才启动,确保网络可用
  • User:指定运行该服务的用户,这里是 kpmark。服务会以该用户的权限运行
  • WorkingDirectory:设置服务的工作目录,这里是 /home/kpmark/nav。服务启动时会切换到该目录
  • ExecStart:指定启动服务的命令,这里是使用 Python 3 执行 /home/kpmark/nav/status.py 脚本
  • Restart:定义服务在什么情况下重启。always 表示无论服务因何原因退出,都会自动重启,确保服务持续运行
  • WantedBy:指定服务的启动级别或目标。multi-user.target 是多用户环境的目标,表示该服务会在系统启动时自动启动

然后,我们通过以下命令启动该服务

sudo systemctl daemon-reload
sudo systemctl enable raspstatus
sudo systemctl start raspstatus

出现问题的话,可以通过以下命令调出服务日志进行查看

# 查看服务日志
sudo journalctl -u raspstatus -f

Nginx 配置

由于涉及到后端的跨域请求,这里需要对 Nginx 进行一些配置

# 状态API代理
location /status {
        proxy_pass http://127.0.0.1:3001/status;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # CORS 配置
        add_header 'Access-Control-Allow-Origin' 'https://www-blog.u2330056.nyat.app:49191';
        add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://www-blog.u2330056.nyat.app:49191';
            add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }

这里我们直接将 /status转发至后端,这样就不用在前端写出完整的请求路由了

  • proxy_pass:将匹配的请求代理到 http://127.0.0.1:3001/status,即转发到本地的 3001 端口上
  • proxy_set_header:设置代理请求的 Host 头为原始请求的 Host 值,确保后端服务能正确识别请求的域名
  • proxy_set_header:设置代理请求的 X-Real-IP 头为客户端 IP 地址,方便后端服务获取真实 IP
  • add_header:设置响应头 Access-Control-Allow-Origin,允许来自 https://www-blog.u2330056.nyat.app:49191 的跨域请求
  • add_header:设置响应头 Access-Control-Allow-Methods,允许的 HTTP 方法为 GETOPTIONS
  • add_header:设置响应头 Access-Control-Allow-Headers,允许的请求头包括 DNTUser-Agent
  • add_header:设置响应头 Access-Control-Expose-Headers,允许客户端访问 Content-LengthContent-Range 头信息 还有一堆 OPTIONS请求处理,这里不再赘述其含义

前端实现

这里给出我的前端 demo,仅供参考

<div class="card status-card">
  <h2>树莓派系统状态</h2>
  <p>实时系统资源监控</p>
  <div class="status-grid">
    <div class="status-item">
      <h3>CPU 使用率</h3>
      <p id="cpu-percent">--</p>
      <div class="status-bar">
        <div class="status-bar-fill" id="cpu-bar" style="width: 0%"></div>
      </div>
    </div>
    <div class="status-item">
      <h3>内存使用率</h3>
      <p id="memory-percent">--</p>
      <div class="status-bar">
        <div class="status-bar-fill" id="memory-bar" style="width: 0%"></div>
      </div>
    </div>
    <div class="status-item">
      <h3>CPU 温度</h3>
      <p id="cpu-temp">--</p>
      <div class="status-bar">
        <div class="status-bar-fill" id="temp-bar" style="width: 0%"></div>
      </div>
    </div>
    <div class="status-item">
      <h3>磁盘使用率</h3>
      <p id="disk-percent">--</p>
      <div class="status-bar">
        <div class="status-bar-fill" id="disk-bar" style="width: 0%"></div>
      </div>
    </div>
  </div>
</div>

<script>
  function formatPercent(value) {
    return `${value.toFixed(1)}%`;
  }

  function formatTemp(value) {
    return `${value.toFixed(1)}°C`;
  }

  function updateStatus() {
    // 使用当前域名的status路径
    fetch("/status")
      .then((response) => response.json())
      .then((data) => {
        // Update CPU
        document.getElementById("cpu-percent").textContent = formatPercent(
          data.cpu_percent,
        );
        document.getElementById("cpu-bar").style.width = `${data.cpu_percent}%`;

        // Update Memory
        document.getElementById("memory-percent").textContent = formatPercent(
          data.memory_percent,
        );
        document.getElementById("memory-bar").style.width =
          `${data.memory_percent}%`;

        // Update Temperature
        if (data.cpu_temp !== null) {
          document.getElementById("cpu-temp").textContent = formatTemp(
            data.cpu_temp,
          );
          document.getElementById("temp-bar").style.width =
            `${(data.cpu_temp / 80) * 100}%`;
        }

        // Update Disk
        document.getElementById("disk-percent").textContent = formatPercent(
          data.disk_percent,
        );
        document.getElementById("disk-bar").style.width =
          `${data.disk_percent}%`;
      })
      .catch((error) => console.error("Error fetching status:", error));
  }

  // Update every 2 seconds
  updateStatus();
  setInterval(updateStatus, 2000);
</script>

这里我使用原生页面简单的实现了卡片效果如下,也可以使用现代前端框架实现更加美观的界面

image-20250215225153644