AX86u官方固件温度监控(CPU,WIFI芯片)

不想刷第三方的固件,但是又想要温度监控,正好试试AI编程,于是有了这篇文章。

方案如下:AX86u按频率上报温度信息给服务器后端,服务端后端负责接收处理数据,和前端展示。服务器后端程序用Python编写,跑在NAS的docker中。

AX86u脚本上报

AX86u官方固件中的curl,wget两个命令都不能访问内网服务,外网可以。这里用的是nc命令。

  1. 温度上报
bash 复制代码
#!/bin/sh

# 配置区
# 目标地址已修改为192.168.50.244
TARGET_HOST="192.168.50.244"
TARGET_PORT="50000"
API_PATH="/receive_temp"
CPU_TEMP_FILE="/sys/class/thermal/thermal_zone0/temp"

# 1. CPU温度获取(完全保留原始代码)
CPU_TEMP_RAW=$(cat "$CPU_TEMP_FILE" 2>/dev/null)
CPU_TEMP_C=$(echo "$CPU_TEMP_RAW" | awk '{printf "%.1f", $1/1000}')

# 2. 2.4G WiFi温度获取(完全保留原始代码)
WIFI2G_RAW=$(wl -i $(nvram get wl0_ifname) phy_tempsense 2>/dev/null)
WIFI2G_TEMP_C=$(echo "$WIFI2G_RAW" | awk '{printf "%.1f", $1/2+20}')

# 3. 5G WiFi温度获取(完全保留原始代码)
WIFI5G_RAW=$(wl -i $(nvram get wl1_ifname) phy_tempsense 2>/dev/null)
WIFI5G_TEMP_C=$(echo "$WIFI5G_RAW" | awk '{printf "%.1f", $1/2+20}')

# 4. 生成时间戳+JSON(完全保留原始代码)
TIMESTAMP=$(date "+%s")
JSON_DATA=$(printf '{"timestamp":"%s","cpu_temp":"%s","wifi2g_temp":"%s","wifi5g_temp":"%s"}' \
    "$TIMESTAMP" "$CPU_TEMP_C" "$WIFI2G_TEMP_C" "$WIFI5G_TEMP_C")

# 5. 上报+结果提示(移除输出重定向,直接终端显示)
echo "待上报数据:$JSON_DATA"

# 构造完整的HTTP POST请求
CONTENT_LENGTH=${#JSON_DATA}
HTTP_REQUEST=$(cat <<EOF
POST $API_PATH HTTP/1.1
Host: $TARGET_HOST:$TARGET_PORT
Content-Type: application/json
Content-Length: $CONTENT_LENGTH

$JSON_DATA
EOF
)

# nc指令上报数据
echo "$HTTP_REQUEST" | nc "$TARGET_HOST" "$TARGET_PORT"

# 保留原始结果提示逻辑
if [ $? -eq 0 ]; then
    echo "上报成功"
else
    echo "上报失败(请检查接口)"
fi
  1. 任务调度
bash 复制代码
#!/bin/sh

# 步骤1:提示用户输入执行间隔(分钟)
echo "======================================"
echo "开始管理定时任务"
echo "======================================"
echo "请输入脚本执行间隔(单位:分钟,建议输入10、30等正整数):"
read INTERVAL

# 验证用户输入是否为有效正整数
if ! echo "$INTERVAL" | grep -q "^[1-9][0-9]*$"; then
    echo "输入错误!请输入大于0的正整数,脚本退出。"
    exit 1
fi

# 配置项:修改为你的实际脚本路径(必须是绝对路径)
SCRIPT_PATH="/jffs/scripts/tempReport.sh"
TASK_NAME="push_temp"

# 步骤2:删除旧的同名定时任务(若存在)
echo ""
echo "[步骤1:删除旧任务(若存在)]"
cru d $TASK_NAME

# 验证删除结果(通过查看任务列表判断)
if ! cru l | grep -q "$TASK_NAME"; then
    echo "旧任务已成功删除(或原本无同名任务)"
else
    echo "旧任务删除失败,将强制继续添加新任务"
fi

# 步骤3:验证目标推送脚本是否存在
echo ""
echo "[步骤2:验证推送脚本可用性]"
if [ -f "$SCRIPT_PATH" ]; then
    echo "推送脚本存在:$SCRIPT_PATH"
else
    echo "推送脚本不存在:$SCRIPT_PATH,请检查路径是否正确"
    exit 1
fi

# 步骤4:给推送脚本赋予执行权限(确保可运行)
if [ ! -x "$SCRIPT_PATH" ]; then
    echo "推送脚本缺少执行权限,正在添加..."
    chmod +x "$SCRIPT_PATH"
    if [ -x "$SCRIPT_PATH" ]; then
        echo "推送脚本执行权限添加成功"
    else
        echo "推送脚本执行权限添加失败,无法继续配置定时任务"
        exit 1
    fi
else
    echo "推送脚本已拥有执行权限"
fi

# 步骤5:添加新的自定义间隔调度定时任务(cru add改为cru a,移除日志输出)
echo ""
echo "[步骤3:添加新的自定义间隔调度任务]"
cru a $TASK_NAME "*/$INTERVAL * * * * /bin/sh $SCRIPT_PATH"

# 步骤6:验证新任务是否添加成功
echo ""
echo "[步骤4:验证新任务配置结果]"
if cru l | grep -q "$TASK_NAME"; then
    echo "新任务已成功添加!"
    echo ""
    echo "任务详情:"
    cru l | grep "$TASK_NAME"
    echo ""
    echo "后续可执行 'cru l' 查看任务列表"
else
    echo "新任务添加失败,请检查命令格式或固件是否支持cru命令"
fi

echo ""
echo "======================================"
echo "定时任务配置流程结束"
echo "======================================"

路径放在/jffs/scripts下面,只需要执行一次任务调度脚本即可。

服务端

app.py

python 复制代码
from flask import Flask, request, render_template_string
from flask_sqlalchemy import SQLAlchemy
import datetime
import io
import base64
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter, HourLocator

app = Flask(__name__)
# 配置SQLite数据库
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////db/temp_data.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

# 定义数据库模型
class TempRecord(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    timestamp = db.Column(db.String(30), nullable=False)  # 存储格式化后的时间字符串
    cpu_temp = db.Column(db.Float, nullable=False)
    wifi2g_temp = db.Column(db.Float, nullable=False)
    wifi5g_temp = db.Column(db.Float, nullable=False)

# 初始化数据库
with app.app_context():
    db.create_all()

# 接收路由器推送数据的接口
@app.route('/receive_temp', methods=['POST'])
def receive_temp():
    try:
        data = request.get_json()
        # 直接使用设备上报的时间并格式化为YYYY-MM-DD HH:MM:SS
        record = TempRecord(
            timestamp=datetime.datetime.fromtimestamp(float(data['timestamp'])).strftime('%Y-%m-%d %H:%M:%S'),
            cpu_temp=float(data['cpu_temp']),
            wifi2g_temp=float(data['wifi2g_temp']),
            wifi5g_temp=float(data['wifi5g_temp'])
        )
        db.session.add(record)
        db.session.commit()
        return "Data saved to DB successfully", 200
    except Exception as e:
        return f"Error: {str(e)}", 500

# 删除数据接口
@app.route('/delete_data', methods=['POST'])
def delete_data():
    try:
        data = request.get_json()
        if data.get('delete_all'):
            TempRecord.query.delete()
        else:
            start_time = data.get('start_time')
            end_time = data.get('end_time')
            if start_time and end_time:
                TempRecord.query.filter(
                    TempRecord.timestamp >= start_time,
                    TempRecord.timestamp <= end_time
                ).delete()
        db.session.commit()
        return "Data deleted successfully", 200
    except Exception as e:
        return f"Error: {str(e)}", 500

# Web可视化页面
@app.route('/')
def index():
    # 获取时间范围参数
    time_range = request.args.get('range', '1d')
    start_time = request.args.get('start_time')
    end_time = request.args.get('end_time')

    # 计算时间范围
    now = datetime.datetime.now()
    if time_range == '1d':
        delta = datetime.timedelta(days=1)
    elif time_range == '3d':
        delta = datetime.timedelta(days=3)
    elif time_range == '5d':
        delta = datetime.timedelta(days=5)
    elif time_range == '1w':
        delta = datetime.timedelta(weeks=1)
    else:
        delta = datetime.timedelta(days=1)
    
    # 计算起始时间
    start_dt = now - delta
    start_str = start_dt.strftime('%Y-%m-%d %H:%M:%S')

    # 根据参数筛选数据
    if start_time and end_time:
        records = TempRecord.query.filter(
            TempRecord.timestamp >= start_time,
            TempRecord.timestamp <= end_time
        ).order_by(TempRecord.id.desc()).all()
    else:
        records = TempRecord.query.filter(
            TempRecord.timestamp >= start_str
        ).order_by(TempRecord.id.desc()).all()

    # 获取最近一条数据用于单独展示
    latest_record = TempRecord.query.order_by(TempRecord.id.desc()).first()

    # 准备图表数据
    img_base64 = ""
    if records:
        # 反转数据,让图表按时间正序显示
        records.reverse()
        
        # 关键修改1:将字符串格式的timestamp转换为datetime对象(适配matplotlib时间刻度)
        timestamps_str = [r.timestamp for r in records]
        timestamps_dt = [datetime.datetime.strptime(ts, '%Y-%m-%d %H:%M:%S') for ts in timestamps_str]
        
        # 提取温度数据(保持原有逻辑不变)
        cpu_temps = [r.cpu_temp for r in records]
        wifi2g_temps = [r.wifi2g_temp for r in records]
        wifi5g_temps = [r.wifi5g_temp for r in records]

        # 生成温度折线图
        plt.figure(figsize=(12, 6))
        # 关键修改2:使用转换后的datetime对象绘图
        plt.plot(timestamps_dt, cpu_temps, label='CPU Temp (°C)', marker='o', markersize=4)
        plt.plot(timestamps_dt, wifi2g_temps, label='2.4G WiFi Temp (°C)', marker='^', markersize=4)
        plt.plot(timestamps_dt, wifi5g_temps, label='5G WiFi Temp (°C)', marker='*', markersize=4)
        
        # 关键修改3:配置X轴,按每小时显示一个标签
        ax = plt.gca()  # 获取当前坐标轴对象
        ax.xaxis.set_major_locator(HourLocator(interval=1))  # 每1小时显示一个主刻度
        ax.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d %H:%M'))  # 时间格式:年-月-日 时:分
        ax.spines['top'].set_visible(False)  # 隐藏上部边框
        ax.spines['right'].set_visible(False)  # 隐藏右侧边框
        
        # 原有标签与样式(保留不变,优化旋转对齐)
        plt.xlabel(' ')
        plt.ylabel('Temperature (°C)')
        plt.title('AX86u Temperature History')
        plt.legend()
        plt.xticks(rotation=45, ha='right')  # ha='right'让旋转后的标签更贴近刻度线
        plt.tight_layout()

        # 将图表转为base64编码(保持原有逻辑不变)
        img_buffer = io.BytesIO()
        plt.savefig(img_buffer, format='png', dpi=100, bbox_inches='tight')
        img_buffer.seek(0)
        img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
        plt.close()

    # 渲染HTML页面(保持原有逻辑不变)
    html_template = '''
    <!DOCTYPE html>    
		<html>
    <head>
    <title>AX86u Temperature Monitor</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
    * { box-sizing: border-box; margin: 0; padding: 0; font-family: "Segoe UI", "Roboto", "Arial", sans-serif; }
    body { margin: 0 auto; padding: 20px 10px; background-color: #f0f4f8; max-width: 1000px; }
    .container { background: #ffffff; border-radius: 18px; padding: 24px 16px; box-shadow: 0 3px 25px rgba(0, 0, 0, 0.05); margin-bottom: 20px; width: 100%; max-width: 1200px; }
    h1 { color: #111827; margin-bottom: 32px; text-align: center; font-size: 30px; font-weight: 700; letter-spacing: 0.5px; }
    h2 { color: #374151; margin-bottom: 20px; text-align: center; font-size: 22px; font-weight: 600; }
    .section-card { margin: 30px 0; padding: 16px; background-color: #f9fafb; border-radius: 10px; border: 1px solid #e5e7eb; }
    .controls { display: flex; flex-wrap: wrap; gap: 16px; justify-content: center; align-items: center; margin-bottom: 30px; }
    .controls > * { padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; font-size: 15px; flex: 1; min-width: 180px; background-color: #ffffff; transition: all 0.2s ease; }
    .controls button { background-color: #2563eb; color: #ffffff; cursor: pointer; border: 1px solid #2563eb; border-radius: 10px; font-weight: 500; }
    .controls button:hover { background-color: #1d4ed8; border-color: #1e40af; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2); }
    .data-management-card { margin: 30px 0; padding: 16px; background-color: #f9fafb; border-radius: 10px; border: 1px solid #e5e7eb; }
    .toggle-delete-section { width: 100%; padding: 10px; background-color: #f8fafc; color: #1f2937; border: 1px solid #e5e7eb; border-radius: 10px; font-size: 16px; cursor: pointer; transition: all 0.3s ease; text-align: center; font-weight: 500; margin-bottom: 16px; }
    .toggle-delete-section:hover { background-color: #e2e8f0; border-color: #cbd5e1; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); }
    .delete-section { display: none; padding: 16px; background-color: #fffafb; border-radius: 10px; border: 1px solid #fecdd3; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03); }
    .data-item { display: flex; justify-content: space-between; align-items: center; margin: 12px 0; font-size: 16px; padding: 8px 0; width: 100%; border-bottom: 1px solid #f3f4f6; }
    .data-item:last-child { border-bottom: none; }
    .data-label { font-weight: 600; text-align: left; color: #1f2937; flex-shrink: 0; }
    .data-value { text-align: right; color: #4b5563; flex-shrink: 0; }
    .chart-container { width: 100%; margin: 0 auto; padding: 0; min-height: auto !important; height: auto !important; display: block; }
    img { max-width: 100%; width: 100%; height: auto; border-radius: 10px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); display: block; margin: 0 auto; }
    .no-chart-tip { text-align: center; color: #6b7280; padding: 10px 0; margin: 0; font-size: 16px; }
    input[type="datetime-local"] { -webkit-appearance: none; -moz-appearance: none; appearance: none; min-width: 0; box-sizing: border-box; background-color: #ffffff; }
    input:focus, select:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); }
    table { width: 100%; border-collapse: collapse; margin-top: 30px; border-radius: 10px; overflow: hidden; box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05); }
    th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #e5e7eb; }
    th { background-color: #2563eb; color: #ffffff; font-weight: 500; }
    tr:hover { background-color: #f9fafb; transition: background-color 0.2s ease; }
    @media (max-width: 768px) {
    .container { padding: 16px 12px; max-width: 100%; }
    .controls { flex-direction: column; align-items: stretch; }
    .controls > * { min-width: unset; width: 100%; }
    h1 { font-size: 26px; }
    h2 { font-size: 20px; }
    .data-item { font-size: 15px; }
    .chart-container { padding: 0; width: 100%; min-height: auto !important; }
    .no-chart-tip { font-size: 15px; }
    }
    </style>
    </head>
    <body>
    <div class="container">
        <h1>AX86u 温度监控</h1>
        <div class="section-card">
            <h2>当前最新数据</h2>
            {% if latest_record %}
                <div class="data-item"><span class="data-label">上报时间:</span><span class="data-value">{{ latest_record.timestamp }}</span></div>
                <div class="data-item"><span class="data-label">CPU温度:</span><span class="data-value">{{ latest_record.cpu_temp }} °C</span></div>
                <div class="data-item"><span class="data-label">2.4G WiFi温度:</span><span class="data-value">{{ latest_record.wifi2g_temp }} °C</span></div>
                <div class="data-item"><span class="data-label">5G WiFi温度:</span><span class="data-value">{{ latest_record.wifi5g_temp }} °C</span></div>
            {% else %}
                <p class="no-chart-tip">暂无数据</p>
            {% endif %}
        </div>
        <div class="section-card">
            <h2>数据筛选与趋势图</h2>
            <div class="controls">
                <select id="timeRange">
                    <option value="1d" {% if time_range == '1d' %}selected{% endif %}>最近1天</option>
                    <option value="3d" {% if time_range == '3d' %}selected{% endif %}>最近3天</option>
                    <option value="5d" {% if time_range == '5d' %}selected{% endif %}>最近5天</option>
                    <option value="1w" {% if time_range == '1w' %}selected{% endif %}>最近1周</option>
                    <option value="custom">自定义时间</option>
                </select>
                <input type="datetime-local" id="startTime" value="{{ start_time }}" style="display: none;">
                <input type="datetime-local" id="endTime" value="{{ end_time }}" style="display: none;">
                <button onclick="applyFilter()">应用筛选</button>
            </div>
            <div class="chart-container">
                {% if img_base64 %}
                    <img src="data:image/png;base64,{{ img_base64 }}" alt="Temperature Chart">
                {% else %}
                    <p class="no-chart-tip">暂无图表数据</p>
                {% endif %}
            </div>
        </div>
        <div class="data-management-card">
            <button class="toggle-delete-section" onclick="toggleDeleteSection()">📋 数据管理</button>
            <div class="delete-section" id="deleteSection">
                <h2>数据管理</h2>
                <div class="controls">
                    <button onclick="deleteAllData()">删除所有数据</button>
                    <input type="datetime-local" id="delStartTime">
                    <input type="datetime-local" id="delEndTime">
                    <button onclick="deleteRangeData()">删除指定时间段数据</button>
                </div>
            </div>
        </div>
    </div>
    <script>
    const timeRangeEl = document.getElementById('timeRange'), startTimeEl = document.getElementById('startTime'), endTimeEl = document.getElementById('endTime'), deleteSectionEl = document.getElementById('deleteSection'), toggleDeleteBtnEl = document.querySelector('.toggle-delete-section');
    timeRangeEl.addEventListener('change', function() {
        const isCustom = this.value === 'custom';
        startTimeEl.style.display = isCustom ? 'block' : 'none';
        endTimeEl.style.display = isCustom ? 'block' : 'none';
    });
    function applyFilter() {
        const range = timeRangeEl.value;
        let url = `/?range=${range}`;
        if (range === 'custom') {
            const start = startTimeEl.value, end = endTimeEl.value;
            if (start && end) {
                const startStr = start.replace('T', ' ') + ':00', endStr = end.replace('T', ' ') + ':00';
                url += `&start_time=${encodeURIComponent(startStr)}&end_time=${encodeURIComponent(endStr)}`;
            }
        }
        window.location.href = url;
    }
    function resetBtnState(btn) { btn.disabled = false; btn.style.backgroundColor = "#2563eb"; }
    function deleteAllData() {
        const btn = event.currentTarget;
        btn.disabled = true;
        btn.style.backgroundColor = "#6b7280";
        if (confirm('确定要删除所有数据吗?此操作不可恢复!')) {
            fetch('/delete_data', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ delete_all: true })
            }).then(response => {
                if (response.ok) { alert('所有数据已删除'); window.location.reload(); }
                else { resetBtnState(btn); }
            }).catch(error => { resetBtnState(btn); console.error('Delete error:', error); });
        } else { resetBtnState(btn); }
    }
    function deleteRangeData() {
        const btn = event.currentTarget;
        btn.disabled = true;
        btn.style.backgroundColor = "#6b7280";
        const start = document.getElementById('delStartTime').value, end = document.getElementById('delEndTime').value;
        if (!start || !end) { alert('请选择完整的时间范围!'); resetBtnState(btn); return; }
        if (confirm('确定要删除该时间段的数据吗?此操作不可恢复!')) {
            const startStr = start.replace('T', ' ') + ':00', endStr = end.replace('T', ' ') + ':00';
            fetch('/delete_data', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ start_time: startStr, end_time: endStr, delete_all: false })
            }).then(response => {
                if (response.ok) { alert('指定时间段数据已删除'); window.location.reload(); }
                else { resetBtnState(btn); }
            }).catch(error => { resetBtnState(btn); console.error('Delete error:', error); });
        } else { resetBtnState(btn); }
    }
    window.onload = function() { if (timeRangeEl.value === 'custom') { startTimeEl.style.display = 'block'; endTimeEl.style.display = 'block'; } };
    function toggleDeleteSection() {
        const isHidden = deleteSectionEl.style.display === 'none' || deleteSectionEl.style.display === '';
        deleteSectionEl.style.display = isHidden ? 'block' : 'none';
        toggleDeleteBtnEl.textContent = isHidden ? '📋 收起数据管理' : '📋 数据管理';
    }
    </script>
    </body>
    </html>
    '''

    return render_template_string(
        html_template,
        img_base64=img_base64,
        latest_record=latest_record,
        time_range=time_range,
        start_time=start_time,
        end_time=end_time
    )

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)

docker配置文件

docker-compose.yml

python 复制代码
version: '3'
services:
  temp-monitor:
    # 使用第三方Python镜像,官方无法访问
    image: docker.1ms.run/library/python:3.11-slim
    container_name: ax86u-temp-monitor
    restart: always
    ports:
      # 宿主机端口:容器端口 → 可自定义宿主机端口,如8081:5000
      - "50000:5000"
    volumes:
      # 挂载代码目录到容器内
      - ./app:/app
      # 挂载数据库目录到容器内,实现数据持久化
      - ./db:/db
    working_dir: /app
    command: >
      sh -c "pip install --no-cache-dir -r requirements.txt && python app.py"
    environment:
      - TZ=Asia/Shanghai

requirements.txt

python 复制代码
flask==3.0.0
flask-sqlalchemy==3.1.1
matplotlib==3.8.2
python-dateutil==2.8.2

其它

python 复制代码
#################### docker 文件件结构 ###################
temp_monitor
    /app
        --app.py
        --requirements.txt
    /db
--docker-compose.yml

#################### 运行docker ##########################
# 建立
sudo docker-compose up -d
# 停止并清理
sudo docker-compose down
# 查看容器实时日志
sudo docker-compose logs -f
# 停止
sudo docker-compose stop
# 开始
sudo docker-compose start

##########################################################

总结:

  1. 全程问的豆包,有些智能,有些傻,提问很重要。

  2. curl和wget一直不能访问内网,研究了很久,豆包也没给出什么好的方法,偶然发现nc能行。

  3. 体验了一天,索然无味,浪费资源。

相关推荐
诗词在线5 小时前
适合赞美风景的诗词名句汇总
python·风景
2401_841495645 小时前
【LeetCode刷题】删除链表的倒数第N个结点
数据结构·python·算法·leetcode·链表·遍历·双指针
Non-existent9875 小时前
地理空间数据处理指南 | 实战案例+代码TableGIS
人工智能·python·数据挖掘
optimistic_chen5 小时前
【Docker入门】Docker Registry(镜像仓库)
linux·运维·服务器·docker·容器·镜像仓库·空间隔离
xj7573065335 小时前
python中的序列化
服务器·数据库·python
郝学胜-神的一滴5 小时前
机器学习特征选择:深入理解移除低方差特征与sklearn的VarianceThreshold
开发语言·人工智能·python·机器学习·概率论·sklearn
却道天凉_好个秋5 小时前
Tensorflow数据增强(一):图片的导入与显示
人工智能·python·tensorflow
木卫二号Coding6 小时前
Docker-构建自己的Web-Linux系统-镜像kasmweb/ubuntu-jammy-desktop
linux·ubuntu·docker
ONExiaobaijs6 小时前
Java jdk运行库合集
java·开发语言·python