🍓 第十五章 硬件接口实验范例(一):树莓派 WiFi 遥控视频车
📄 一句话概括

13-2 的 Flask 只开了一个开关,本节把它升级成能同时伺候【动作指令】和【视频流】的控制台——关键是 threaded 模式,以及绕开 Flask 3 已经删掉的老写法。

📝 本节和前面几节的关系

13-2 用 Flask + gpiozero 做了一次浏览器点灯,那是 Flask 的最小形态;本节回到同一个框架,补上它做【遥控台】必需的两块能力:多个路由协同与并发处理长响应。15-4 会直接把本节的成果拼上 15-2 的电机和 10 章的相机。

一、为什么还要回头讲 Flask

13-2 那次是【一个页面、一个按钮、一次请求】——请求发出去、GPIO 切一下、响应立刻返回,几十毫秒的事。遥控车完全不同,它有两个特殊要求:

  1. 动作要分得细:前进、后退、左转、右转、停车、调速,每个都要一个独立入口——这需要多个路由;
  2. 有一条请求永远不返回:MJPEG 视频流是【服务器不停地往浏览器推帧】,一个响应持续几分钟甚至几小时——这条请求会独占一个连接线程。

第 2 条是本节真正的技术核心。

二、路由:给每个动作开一个门

Flask 用装饰器把 URL 和函数绑在一起:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.get("/")                 # 首页:返回控制面板 HTML
def index():
    return PANEL_HTML

@app.post("/move/<direction>")  # 动作路由:/move/forward 等
def move(direction):
    speed = float(request.args.get("speed", 0.6))   # 从查询串取速度
    # 这里调用 15-2 的电机对象
    return jsonify({"action": direction, "speed": speed})

三种常用参数位置,别混:

位置 写法 典型用途
路径参数 /move/<direction> 动作名(forward/stop)
查询串 /move/forward?speed=0.8 可选的数值(速度)
表单/JSON 体 request.get_json() 复杂结构(本章用不上)

HTTP 方法也要讲规矩:查询用 GET,改变硬件状态的动作用 POST——这样浏览器预取、刷新页面时不会误发动作(否则刷新一下车就冲出去)。

三、threaded=True:本节的生命线

Flask 自带的开发服务器默认是单线程的:一个请求处理完才接下一个。把视频流接上去,灾难立刻发生——视频流那条响应【永不结束】,服务器就永远在处理它,所有动作请求全排队,你的车就会【按了按钮半天没反应】,甚至按了完全不动。

解法一行:

app.run(host="0.0.0.0", port=5000, threaded=True)

threaded=True 让每个请求各占一个线程:视频流自己占一条,动作请求随时能挤进来。这是遥控车能不能开起来的分水岭——不开这个参数,代码全对、车就是不动。

补充两个参数的含义:

四、老教程里已失效的写法(Flask 3 换代)

Flask 现在的主版本是 3.x(3.1.3 于 2026 年 2 月发布)。3.0 一次性删掉了一批老教程里的常客,照抄必报错:

老写法 现在的替代 说明
@app.before_first_request 应用工厂里初始化,或 with app.app_context(): 3.0 已移除,报错说没有这个属性
FLASK_ENV=development FLASK_DEBUG=1 或 flask run --debug 环境变量已删,设了也不会开调试
app.config["JSON_AS_ASCII"] 等 app.json.ensure_ascii = False JSON 配置键全家已删
send_file(..., attachment_filename=) download_name= 参数 2.0 改名、3.0 收尾,旧名直接 TypeError

另一个必须知道的事实:app.run() 启动的是 Werkzeug 开发服务器,官方明确说它不能用于生产。本课所有场景都在自家局域网内调试,用它没问题;但绝不要把它暴露到公网——没有并发防护、没有安全加固,等于把车和树莓派一起送人。

五、安全提醒:局域网内也别忘了关门

Flask 服务默认没有任何认证——同一个 WiFi 下任何人打开 http://<树莓派IP>:5000 都能开你的车。三条最低限度的防护:

  1. 改端口(比如 8000):躲开扫描器的默认目标;
  2. 加一个简单口令:路由里查 request.args.get("key") 是否匹配;
  3. 只在家里 WiFi 用,不要做端口转发映射到公网。

六、动手 Demo:三路由控制台(不接电机也能跑)

先不接电机,只验证路由与并发——路由通了,接上电机就是 15-4:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.get("/")
def index():
    return "<h1>控制台</h1><p>POST /move/forward?speed=0.6 试试</p>"

@app.post("/move/<direction>")
def move(direction):
    speed = float(request.args.get("speed", 0.6))
    print("收到动作:", direction, "速度:", speed)   # 用日志代替真实电机
    return jsonify({"action": direction, "speed": speed})

@app.get("/status")
def status():
    return jsonify({"ok": True})

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

在树莓派上跑起来(python3 app.py),然后开两个终端分别测:

# 终端 A:模拟视频流——一条永不结束的响应
curl -N http://127.0.0.1:5000/status

# 终端 B:动作请求
curl -X POST "http://127.0.0.1:5000/move/forward?speed=0.6"
curl -X POST "http://127.0.0.1:5000/move/stop"

看到什么算成功:终端 B 的动作请求立刻返回:

{"action": "forward", "speed": 0.6}

同时树莓派的控制台打印【收到动作: forward 速度: 0.6】。

读结果:把 threaded=True 改成 False 再重跑一遍上面的测试——动作请求会卡住直到终端 A 断开为止。亲眼看一次这个对比,胜过记十遍结论。

30 秒实验

在树莓派上开三个终端,同时(用 & 或三个窗口)各发一次请求:

for i in 1 2 3; do curl -s -X POST "http://127.0.0.1:5000/move/forward?speed=0.$i" & done; wait

三条请求会几乎同时返回(顺序可能交错)。这证明线程模式下请求是并发处理的——这也意味着:两个用户同时按下【前进】和【后退】,服务器会先后执行两条指令,最终状态以后到的为准。多人同时遥控同一台车,逻辑上必然打架,这也是本章不做多用户支持的原因。

七、常见误区

  1. 路由写好了,车就是不动,怀疑接错线 —— 先查 threaded=True。视频流占着唯一线程时,动作请求根本得不到执行,接线再对也没用。
  2. 用 GET 请求做动作 —— 浏览器的预取、后退、刷新都可能重发 GET;动作类接口一律 POST,避免【刷新页面车就跑】。
  3. 老教程里 before_first_request 抄来就能用 —— Flask 3.0 已删除该装饰器;初始化代码写进应用工厂或 with app.app_context(): 块。
  4. 反正家里局域网,不用管安全 —— 同一 WiFi 下任何设备都能访问,别把服务暴露到公网;加个口令、换个端口是最低成本的自保。
自测三问(点开对答案) 1. `threaded=True` 为什么是遥控车项目的必需项? 视频流是永不结束的响应,会独占线程;单线程模式下所有动作请求都会被它堵死,车无法响应。 2. 动作接口为什么用 POST 而不是 GET? GET 会被浏览器预取、刷新、历史回退触发重发;动作用 POST 可避免【刷新一下车就冲出去】这类事故。 3. Flask 3 里 `FLASK_ENV=development` 还有用吗? 没有,该环境变量已在 3.0 移除;改用 `FLASK_DEBUG=1` 或 `flask --app app run --debug`。

相关笔记