关于换源:

直接覆盖写入 sources.list

1
2
3
4
5
6
sudo tee /etc/apt/sources.list > /dev/null << 'EOF'
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware
deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware
EOF

覆盖写入 raspi.list

1
2
3
sudo tee /etc/apt/sources.list.d/raspi.list > /dev/null << 'EOF'
deb https://mirrors.tuna.tsinghua.edu.cn/raspberrypi/ bookworm main
EOF

更新生效

1
2
sudo apt update
sudo apt upgrade -y

关于ssh与vnc

方法一:桌面方式(进入系统后操作)

点击左上角树莓图标 → Preferences → Raspberry Pi Configuration。(树莓派配置)

找到 Interfaces 选项卡。

把 SSH 改成 Enable,点击 OK。

重启树莓派。

vnc也在此处

方法二:命令行方式(有键盘或能进入终端时)

1
2
sudo systemctl enable ssh
sudo systemctl start ssh

方法三:无键盘直接启用(SD 卡修改)

把树莓派 SD 卡插到电脑。

打开 boot 分区(Windows 可见)。

在根目录下新建一个空文件,名字叫 ssh(无扩展名)。

插回树莓派启动,SSH 会自动启用。

(这个方法不太好用,第一次烧录官方镜像总是进初始配置界面,写wpa_supplicant.conf也没用,还是需要键盘和HDMI+OBS应付一下)

建议在烧录软件Raspberry Pi Imager 里,CTRL SHIFT X ,高级设置配置好再烧录。

关于GPIO

wiringpi已经停止更新

gpiozero和RPi.GPIO也可以使用

python随便写一写好了

img

以下是宠物喂食器的记录:

宠物喂食器项目方案(树莓派 + Flask + Ngrok + GPIO)

1. 项目概述

本项目目标是在树莓派上实现一个宠物喂食器,可通过手机网页控制喂食时间、次数、分量(全份或半份)以及临时投喂。
每次喂食操作将调用控制步进电机的 GPIO 脚本。

参考模型 : mdenisov/feeder:宠物喂食器 — mdenisov/feeder: Pet feeder

功能包括:

  • 设置定时喂食时间表
  • 设置每次喂食的份量(全份/半份)
  • 临时喂食
  • 通过手机浏览器控制(支持 Android/iOS)
  • 开机自启
  • 使用 Ngrok 生成可公网访问的 URL(可绑定免费静态子域名)

2. 树莓派环境准备

2.1 系统和 Python

  • 推荐树莓派 OS 64-bit
  • Python3 已预装(建议 >=3.11)
  • 安装必要库:
1
2
sudo apt update
sudo apt install python3-pip python3-venv git nano -y

2.2 虚拟环境(避免 system package 错误)

没用这个)

2.3 安装 Python 库

1
pip install flask flask-cors gpiozero apscheduler

注:gpiozero 用于控制 GPIO;apscheduler 用于定时任务;flask-cors 允许网页跨域访问。

如果pip报错:
This environment is externally managed. To install Python packages system-wide, try apt install python3-xyz, where xyz is the package you are trying to install. If you wish to install a non-Debian-packaged Python package, create a virtual environment using python3 -m venv path/to/venv. Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make sure you have python3-full installed.

这是 Debian 12 / Raspberry Pi OS Bookworm 的新机制,默认不让 pip 直接改系统 Python 环境。解决办法有三个:

  1. 推荐:建虚拟环境

    1
    2
    3
    4
    sudo apt install python3-venv -y
    python3 -m venv ~/myenv
    source ~/myenv/bin/activate
    pip install flask gpiozero

    以后运行程序要先 source ~/myenv/bin/activate,退出用 deactivate

  2. 快速方案:pip 强行安装

    1
    pip install flask gpiozero --break-system-packages

    这是官方允许的“危险选项”,会绕过限制(大多数人其实都这么干)。

  3. 用 apt 装系统自带包(我就用这个省事的方法了)

    1
    2
    sudo apt update
    sudo apt install python3-flask python3-gpiozero -y

3. GPIO 控制脚本(gpio.py)

  • 控制步进电机转动一定步数
  • 每次喂食一份调用两次,半份调用一次

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# gpio_feed.py
import RPi.GPIO as GPIO
import time

# 定义 GPIO 引脚
DIR_PIN = 17 # PWM 输出
ENA_PIN = 27 # 使能口
FIX_PIN = 22 # 始终为 0

# 初始化 GPIO
def setup_gpio():
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(DIR_PIN, GPIO.OUT)
GPIO.setup(ENA_PIN, GPIO.OUT)
GPIO.setup(FIX_PIN, GPIO.OUT)

# 初始安全状态
GPIO.output(ENA_PIN, GPIO.LOW)
GPIO.output(FIX_PIN, GPIO.LOW)

# feed 函数:直接调用即可
def feed():
setup_gpio()
pwm = GPIO.PWM(DIR_PIN, 7500) # 6000 Hz
try:
print("Enable motor (GPIO27 = HIGH)")
GPIO.output(ENA_PIN, GPIO.HIGH)

print("Start PWM on GPIO17 (6000Hz, 50%)")
pwm.start(50) # 占空比 50%

# 运行一段时间,比如 2 秒
time.sleep(0.25)

print("Stop PWM")
pwm.stop()

print("Disable motor (GPIO27 = LOW)")
GPIO.output(ENA_PIN, GPIO.LOW)

# FIX_PIN 保持 0
GPIO.output(FIX_PIN, GPIO.LOW)
print("GPIO22 = 0 保持不变")

except Exception as e:
print("feed() 出错:", e)
finally:
GPIO.cleanup()

if __name__ == "__main__":
feed()

注意:实际步进电机控制可以替换 LED 为步进电机库函数或直接用 gpiozero.OutputDevice


4. Flask Web 服务器(app.py)

  • 提供网页接口设置喂食时间、次数、分量
  • 调用 gpio.py 执行喂食

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
from flask import Flask, render_template, request, redirect, url_for
import threading, time, json, os
from datetime import datetime
import gpio

app = Flask(__name__)

SCHEDULE_FILE = "scheduled_jobs.json"
HISTORY_FILE = "feeding_history.json"

# ---------------- 文件操作 ----------------
def load_schedules():
if os.path.exists(SCHEDULE_FILE):
with open(SCHEDULE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return []

def save_schedules(schedules):
with open(SCHEDULE_FILE, "w", encoding="utf-8") as f:
json.dump(schedules, f, ensure_ascii=False, indent=2)

def load_history():
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return []

def save_history(history):
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=2)

# ---------------- 投喂 ----------------
def feed(portion):
times = 2 if portion=="full" else 1
for _ in range(times):
gpio.feed()
# 写入历史
history = load_history()
history.append({
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"portion": "全份" if portion=="full" else "半份"
})
save_history(history)

# ---------------- 定时任务 ----------------
def scheduler():
done_today = set() # 每天重置
while True:
now = datetime.now()
current_time = now.strftime("%H:%M")
schedules = load_schedules()
for job in schedules:
key = f"{job['time']}_{job['portion']}"
if job["time"] == current_time and key not in done_today:
feed(job["portion"])
done_today.add(key)
if current_time == "00:00":
done_today.clear()
time.sleep(30)

threading.Thread(target=scheduler, daemon=True).start()

# ---------------- 路由 ----------------
@app.route("/", methods=["GET", "POST"])
def index():
if request.method=="POST":
portion = request.form.get("portion")
if portion:
feed(portion)
schedules = load_schedules()
schedules = sorted(schedules, key=lambda x: x["time"])
return render_template("index.html",
scheduled_jobs=load_schedules(),
feeding_history=load_history())

@app.route("/add_schedule", methods=["POST"])
def add_schedule():
time_ = request.form.get("time")
portion = request.form.get("portion")
schedules = load_schedules()
schedules.append({"time": time_, "portion": portion})
save_schedules(schedules)
return redirect(url_for("index"))

@app.route("/delete_schedule/<int:index>", methods=["POST"])
def delete_schedule(index):
schedules = load_schedules()
if 0 <= index < len(schedules):
schedules.pop(index)
save_schedules(schedules)
return redirect(url_for("index"))

@app.route("/clear_history", methods=["POST"])
def clear_history():
save_history([]) # 将 JSON 文件初始化为空列表
return redirect(url_for("index"))

if __name__=="__main__":
app.run(host="0.0.0.0", port=5000)


5. 网页界面(templates/index.html)

  • 简单响应式网页,可在手机浏览器访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宠物喂食器</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f0f8ff; }
h1 { text-align:center; }
section { margin-bottom: 30px; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #aaa; padding: 8px; text-align: center; }
button { padding: 6px 12px; margin: 2px; }
form { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
input[type="time"], select { padding: 5px; }
</style>
</head>
<body>
<h1>宠物喂食器控制台</h1>

<section>
<h2>立即投喂</h2>
<form method="POST">
<select name="portion">
<option value="full">全份</option>
<option value="half">半份</option>
</select>
<button type="submit">立即投喂</button>
</form>
</section>

<section>
<h2>定时投喂任务</h2>
<table>
<tr><th>时间</th><th>份量</th><th>操作</th></tr>
{% for job in scheduled_jobs %}
<tr>
<td>{{ job.time }}</td>
<td>{{ '全份' if job.portion=='full' else '半份' }}</td>
<td>
<form method="POST" action="/delete_schedule/{{ loop.index0 }}">
<button type="submit">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>

<h3>新增任务</h3>
<form method="POST" action="/add_schedule">
<input type="time" name="time" required>
<select name="portion">
<option value="full">全份</option>
<option value="half">半份</option>
</select>
<button type="submit">添加任务</button>
</form>
</section>

<section>
<h2>投喂历史</h2>
<table>
<tr><th>时间</th><th>份量</th></tr>
{% for record in feeding_history|reverse %}
<tr>
<td>{{ record.time }}</td>
<td>{{ record.portion }}</td>
</tr>
{% endfor %}
</table>

<form method="POST" action="/clear_history" style="text-align:center; margin-top:10px;">
<button type="submit" style="background-color:#ff6666; color:white;">清空投喂历史</button>
</form>

</section>

</body>
</html>


6. Ngrok 远程访问

6.1 安装并认证

1
2
3
wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip
unzip ngrok-stable-linux-arm.zip
./ngrok authtoken <你的token> # 这个在文档https://ngrok.com/docs/getting-started/有说明

如果 ngrok version 出现是2.x.x,可能会有以下问题:

grok http 5000 Your ngrok-agent version “2.3.41” is too old. The minimum supported agent version for your account is “3.7.0”. Please update to a newer version with ngrok update, by downloading from https://ngrok.com/download, or by updating your SDK version. Paid accounts are currently excluded from minimum agent version requirements. To begin handling traffic immediately without updating your agent, upgrade to a paid plan: https://dashboard.ngrok.com/billing/subscription.

那么删除旧版本:

1
2
rm -f ngrok
rm -f ngrok-stable-linux-arm.zip

下载 ngrok v3(ARM 版本):
(树莓派通常是 ARMv7 或 ARM64)

  • 如果是 32位树莓派 OS(常见在老款 Pi):

    1
    wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-arm.zip
  • 如果是 64位树莓派 OS

    1
    wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-arm64.zip

解压并安装:

1
2
unzip ngrok-v3-stable-linux-arm*.zip
sudo mv ngrok /usr/local/bin/

再次检查版本:

1
ngrok version

6.2 运行 Flask + Ngrok 脚本(start_flask_ngrok.sh)

1
2
3
4
5
6
7
8
#!/bin/bash
cd /home/wde/Desktop/python
source venv/bin/activate
python app.py &
./ngrok http 5000

# 其实只要nohup /usr/local/bin/ngrok http --domain=(这里是申请的固定域名) 5000(端口号,和app一致) > ngrok.log 2>&1 &

  • 记得给执行权限:
1
chmod +x start_flask_ngrok.sh

6.3 设置开机自启

1
crontab -e

添加:

1
@reboot /home/wde/Desktop/python/start_flask_ngrok.sh

这个方法不太好用,后来把所有功能写在一个main.py里,只用启动一次


7. 开机自启(Autostart)

桌面版 Raspberry Pi OS

  • 程序路径(例如 /home/wde/Desktop/python/main.py)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from flask import Flask, render_template, request, redirect, url_for
import threading, time, json, os, subprocess
from datetime import datetime
import RPi.GPIO as GPIO

# ---------------- GPIO 初始化 ----------------
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

PIN_ENABLE = 27 # 控制电机使能
PIN_PUL = 17 # 步进脉冲输出

GPIO.setup(PIN_ENABLE, GPIO.OUT)
GPIO.setup(PIN_PUL, GPIO.OUT)
GPIO.output(PIN_ENABLE, 0)
GPIO.output(PIN_PUL, 0)

def gpio_feed():
"""投喂电机动作:高频率 PWM,输出 3200 脉冲"""
GPIO.output(PIN_ENABLE, 1) # 使能
pwm = GPIO.PWM(PIN_PUL, 7500) # 7.5kHz
pwm.start(50) # 占空比 50%
time.sleep(3200/7500) # 持续时间 = 脉冲数 / 频率
pwm.stop()
GPIO.output(PIN_ENABLE, 0) # 关闭电机

# ---------------- Flask 应用 ----------------
app = Flask(__name__)

SCHEDULE_FILE = "scheduled_jobs.json"
HISTORY_FILE = "feeding_history.json"

def load_schedules():
if os.path.exists(SCHEDULE_FILE):
with open(SCHEDULE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return []

def save_schedules(schedules):
with open(SCHEDULE_FILE, "w", encoding="utf-8") as f:
json.dump(schedules, f, ensure_ascii=False, indent=2)

def load_history():
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return []

def save_history(history):
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=2)

# ---------------- 投喂 ----------------
def feed(portion):
times = 2 if portion=="full" else 1
for _ in range(times):
gpio_feed()
history = load_history()
history.append({
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"portion": "全份" if portion=="full" else "半份"
})
save_history(history)

# ---------------- 定时任务 ----------------
def scheduler():
done_today = set()
while True:
now = datetime.now()
current_time = now.strftime("%H:%M")
schedules = load_schedules()
for job in schedules:
key = f"{job['time']}_{job['portion']}"
if job["time"] == current_time and key not in done_today:
feed(job["portion"])
done_today.add(key)
if current_time == "00:00":
done_today.clear()
time.sleep(30)

threading.Thread(target=scheduler, daemon=True).start()

# ---------------- 路由 ----------------
@app.route("/", methods=["GET", "POST"])
def index():
if request.method=="POST":
portion = request.form.get("portion")
if portion:
feed(portion)
schedules = load_schedules()
schedules = sorted(schedules, key=lambda x: x["time"])
return render_template("index.html",
scheduled_jobs=schedules,
feeding_history=load_history())

@app.route("/add_schedule", methods=["POST"])
def add_schedule():
time_ = request.form.get("time")
portion = request.form.get("portion")
schedules = load_schedules()
schedules.append({"time": time_, "portion": portion})
save_schedules(schedules)
return redirect(url_for("index"))

@app.route("/delete_schedule/<int:index>", methods=["POST"])
def delete_schedule(index):
schedules = load_schedules()
if 0 <= index < len(schedules):
schedules.pop(index)
save_schedules(schedules)
return redirect(url_for("index"))

@app.route("/clear_history", methods=["POST"])
def clear_history():
save_history([])
return redirect(url_for("index"))

# ---------------- 主程序 ----------------
if __name__=="__main__":
# 启动 Flask
threading.Thread(target=lambda: app.run(host="0.0.0.0", port=5000), daemon=True).start()

# 启动 ngrok
cmd = "nohup /usr/local/bin/ngrok http --domain=shrimp-direct-distinctly.ngrok-free.app 5000 > ngrok.log 2>&1 &"
subprocess.call(cmd, shell=True)

# 保持主线程不退出
while True:
time.sleep(3600)
  • 也可使用使用 systemd 服务crontab

方法

  1. 编辑 autostart 文件
1
sudo nano /etc/xdg/lxsession/LXDE-pi/autostart

注释:lxsession 桌面会话启动时会读取这个文件,实现开机启动程序。

  1. 添加启动命令
1
@lxterminal -e python3 /home/wde/Desktop/python/main.py

注释:

  • @lxterminal 表示打开一个终端窗口运行程序。
  • -e 指定终端执行命令。
  • python3 /home/wde/Desktop/python/main.py 是要运行的 Python 程序。
  1. 保存并退出
  • Ctrl + O → 回车 → Ctrl + X
  1. 效果
  • 开机后桌面会自动打开终端窗口,并执行程序。
  • 如果不希望看到终端窗口,可使用 @/usr/bin/python3 /home/wde/Desktop/python/main.py & 来静默运行。

8. 降低功耗方法

条件

  • WiFi 和 HDMI 保持开启。
  • 禁用不必要外设(蓝牙、音频接口等)。
  • 调整 CPU/GPU 频率和电压。

编辑 config.txt

1
sudo nano /boot/firmware/config.txt

注释:Raspberry Pi 4 桌面版的 boot 配置文件在 /boot/firmware/config.txt,非桌面版仍是 /boot/config.txt

推荐设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# CPU 降频,降低功耗
arm_freq=600 # 设置 CPU 频率为 600MHz
over_voltage=-2 # CPU 电压降低,减少发热

# GPU 降频与内存调整
gpu_freq=200 # GPU 频率
gpu_mem=32 # GPU 占用内存,最小化

# HDMI 设置,保证输出但低功耗
hdmi_force_hotplug=1 # 强制启用 HDMI
hdmi_drive=2 # HDMI 输出模式

# 禁用不必要外设
dtoverlay=disable-bt # 禁用蓝牙
dtparam=audio=off # 禁用板载音频

保存并生效

1
sudo reboot

注释:重启后配置生效,CPU/GPU 降频、外设关闭,减少功耗和发热。


9. 注意事项