上篇文章寫了 GX10 大腦 + Pi 5 現場 I/O 的架構,這篇文章記錄把它真的跑通的完整過程——包含一個花了很久的怪現象:同一支指令,別人能切、我就 busy。
最終成果(先講結論)
一句話 → 地端大模型判定意圖 → HTTP → Pi 5 真實切 GPIO,全程約 1 秒:
$ python3 gx10_ai_controller.py "把燈關了"
✅ Pi5 健康 | 目前: {'devices': {'relay1': 'on'}, ...}
⏱ LLM (gemma4:e4b) 耗時 0.4s
意圖: {"action": "off", "reason": "關燈"}
✏ Pi5 回應: {"dry_run": false, "pin": 22, "state": "off", "status": "success"}
兩端程式(已部署並在跑)
Pi 5 端 — HTTP 控制器(port 5001,systemd 常駐)
import logging, os, threading
from datetime import datetime
from flask import Flask, request, jsonify
DRY_RUN = os.environ.get("DRY_RUN", "0") == "1"
RELAY_PIN = int(os.environ.get("RELAY_PIN", "22"))
ACTIVE_HIGH = os.environ.get("ACTIVE_HIGH", "0") == "1"
logger = ... # 省略
relay = None
if not DRY_RUN:
from gpiozero import OutputDevice
# 低電位觸發繼電器(active_high=False)
relay = OutputDevice(RELAY_PIN, active_high=ACTIVE_HIGH, initial_value=False)
app = Flask(__name__)
@app.route("/api/device", methods=["POST"])
def control_device():
data = request.get_json(silent=True) or {}
action = data.get("action", "").lower()
if action not in ("on", "off"):
return jsonify({"status": "error", "message": "action must be on|off"}), 400
if relay: relay.on() if action == "on" else relay.off()
return jsonify({"status": "success", "state": action, "dry_run": bool(DRY_RUN),
"pin": None if DRY_RUN else RELAY_PIN})
systemd user service(~/.config/systemd/user/pi5-relay.service):
[Service]
Type=simple
Environment=DRY_RUN=0
Environment=RELAY_PIN=22
ExecStart=/home/pi/venv/bin/python3 /home/pi/pi5_relay.py
Restart=always
加上 loginctl enable-linger pi,重開機不需要登入就會自動起服務。
GX10 端 — AI 決策層
SYSTEM_PROMPT = """你是決策樞紐。只回一個 JSON 物件, 格式:
{"action":"on" 或 "off","reason":"簡短說明"}
開/open/turn on→on;關/close/turn off→off;無關控制→{"action":"none"}"""
# 1) 地端 Ollama(gemma4:e4b,temperature=0)解析意圖
# 2) 意圖 → POST http://<pi5>:5001/api/device
# 3) 非 on/off(含 none)一律不下達, 直接回報
踩坑實錄:「GPIO busy」的真相
這是本次最大的坑。現象:Pi 5 上已有一個程式(我稱它 search_bot)能用 gpiozero 正常切繼電器,我寫的新程式卻全部失敗,錯誤一路切換:
| 嘗試 | 結果 |
|---|---|
系統 python + OutputDevice(17) |
KeyError: PinInfo(number=11, ...) |
RPi.GPIO |
Cannot determine SOC peripheral base address |
| sysfs GPIO export | OSError: Invalid argument |
| 換腳位再試 | lgpio.error: 'GPIO busy' |
一個一個排除下來,真相其實是最普通的一種:
GPIO pin 是排他的。search_bot 已經 claim 了 GPIO 14/15/17/18/21(4 顆 relay + 1 顆按鈕),任何新程序再 claim 同一支腳就会 busy。
驗證方式:挑一支 search_bot 沒用的腳(GPIO 22)做同一個測試——
✅ OutputDevice(22) 建立成功
GPIO22 -> on
GPIO22 -> off
--- gpiozero 在自由腳位上工作正常 ---
一次就通。所谓「怪現象」,就是「腳被佔用」。
為什麼 RPi.GPIO 會掛?
Pi 5 有多個 GPIO controller(/dev/gpiomem0~4、多個 gpiochip),RPi.GPIO 舊版要猜「SOC peripheral base」就猜錯;gpiozero + gpiozero/lgpio 走 gpiochip 介面,選對 controller 就正常——這就是同一台機子、同樣的 pin,一個能跑一個掛的原因。
三個實戰原則
- GPIO 是排他資源 — 多程序共用在 Pi 上一定會撞;分配腳位前,先用
journalctl或pids+ 程式碼 grep 看誰已經 claim 了 - DRY_RUN 是上線安全閘 — 新控制器先跑 DRY_RUN=1,API 鏈路全通、GPIO 不碰,確認無誤再切 0
- 選對 pin factory — Pi 5 上
RPi.GPIO(舊)不穩;gpiozero + lgpio backend是現行正解
可復用清單
| 項目 | 值 |
|---|---|
| Pi 5 服務 | systemctl --user status pi5-relay |
| 狀態 API | GET http://<pi5>:5001/api/status |
| 下達 API | POST /api/device {"action":"on\|off"} |
| 換腳位 | service RELAY_PIN= 改數字,restart 即可 |
| 反相 | ACTIVE_HIGH=1 |
| 回滾 DRY_RUN | DRY_RUN=1,API 鏈路照跑、不碰 pin |
結語
「別人能跑、我不能跑」的坑,90% 是資源已被佔用而不是環境壞掉。這次的 GPIO busy 就是如此:把「腳位排他性」這個前提寫進腦子,下次一眼就能看出誰跟誰撞了。
沒有留言:
張貼留言