关于换源: 直接覆盖写入 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 sshsudo 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随便写一写好了
以下是宠物喂食器的记录: 宠物喂食器项目方案(树莓派 + 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 updatesudo 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 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。
快速方案:pip 强行安装
1 pip install flask gpiozero --break-system-packages
这是官方允许的“危险选项”,会绕过限制(大多数人其实都这么干)。
用 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 import RPi.GPIO as GPIOimport timeDIR_PIN = 17 ENA_PIN = 27 FIX_PIN = 22 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) def feed (): setup_gpio() pwm = GPIO.PWM(DIR_PIN, 7500 ) try : print ("Enable motor (GPIO27 = HIGH)" ) GPIO.output(ENA_PIN, GPIO.HIGH) print ("Start PWM on GPIO17 (6000Hz, 50%)" ) pwm.start(50 ) time.sleep(0.25 ) print ("Stop PWM" ) pwm.stop() print ("Disable motor (GPIO27 = LOW)" ) GPIO.output(ENA_PIN, GPIO.LOW) 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_forimport threading, time, json, osfrom datetime import datetimeimport gpioapp = 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([]) 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/
再次检查版本:
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 @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
方法
编辑 autostart 文件
1 sudo nano /etc/xdg/lxsession/LXDE-pi/autostart
注释:lxsession 桌面会话启动时会读取这个文件,实现开机启动程序。
添加启动命令
1 @lxterminal -e python3 /home/wde/Desktop/python/main.py
注释:
@lxterminal 表示打开一个终端窗口运行程序。
-e 指定终端执行命令。
python3 /home/wde/Desktop/python/main.py 是要运行的 Python 程序。
保存并退出
效果
开机后桌面会自动打开终端窗口,并执行程序。
如果不希望看到终端窗口,可使用 @/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 # 禁用板载音频
保存并生效
注释:重启后配置生效,CPU/GPU 降频、外设关闭,减少功耗和发热。
9. 注意事项