「Raspberry Piで電子工作を始めたいけど、まだ本体が届いていない…」「配線してから動かないと、プログラムが悪いのか配線が悪いのか分からない…」──そんな経験、ありませんか?

実は、Raspberry Pi用の定番ライブラリgpiozeroには、本物のGPIOピンの代わりに「偽物のピン」を使える仕組み(MockFactory)が用意されています。これを使うと、普段使っているWindowsやMacのPCだけで、LED・ボタン・人感センサーのプログラムを動かして確認できます。

難易度はPython初〜中級者向け。Raspberry Piを持っていなくても最後まで手を動かせます😊 この記事では、ボタンでLEDを点ける基本から、人感センサーライトのプログラム、そしてpytestでの自動テストまで、実際に動かした結果と一緒に見ていきましょう!

gpiozeroのMockFactoryとは?

raspberry pi
raspberry pi / Photo by Alessandro Oliverio via Pexels

gpiozeroは、LED(17) や Button(2) のように「部品の名前」でGPIOを扱えるPythonライブラリです。Raspberry Pi OSには最初から入っています。

gpiozeroは、実際にピンを読み書きする部分を「ピンファクトリー」という部品に任せています。MockFactoryはその「偽物版」です。イメージとしては、本物の基板の代わりに、ピンの電圧(HIGH / LOW)をメモリ上で覚えておくだけの仮想基板を差し込む感じです。

  • LEDを on() すると、仮想ピンの状態が HIGH になる
  • ボタンやセンサーの入力は、pin.drive_low() / pin.drive_high() で自分で電圧を変えて再現する
  • 実機に持っていくときは、MockFactoryを指定している行を消すだけ

公式ドキュメントでも「Piが手元にないときにスクリプトを開発するのにも便利」と紹介されている、れっきとした公式機能です(gpiozero公式:API – Pins)。

準備:PCにgpiozeroを入れる

PCではふつうにpipでインストールできます。テストで使うpytestも一緒に入れておきましょう。

pip install gpiozero pytest

この記事のコードは、Windows 11・Python 3.13・gpiozero 2.0.1・pytest 9.1.1 で動作を確認しています。

ステップ1:ボタンを押している間だけLEDを点ける

まずは電子工作の「Hello World」、ボタンとLEDです。実機なら GPIO17 にLED、GPIO2 にボタンをつなぐ想定です。

ここが重要です👇

  • Device.pin_factory = MockFactory() で、以降の部品はすべて仮想ピンを使う
  • Button は標準でプルアップ(押すとGNDにつながる配線)なので、LOW = 押されたになる
  • led.pin.states で、LEDのピンがどう変化してきたかの履歴を確認できる
from gpiozero import Device, LED, Button
from gpiozero.pins.mock import MockFactory

# ★ここがポイント:本物のGPIOの代わりに「偽物のピン」を使う
Device.pin_factory = MockFactory()

led = LED(17)       # GPIO17 に LED をつないだつもり
button = Button(2)  # GPIO2 にボタンをつないだつもり(押すとGNDにつながる)

# ボタンを押している間だけLEDを点ける
button.when_pressed = led.on
button.when_released = led.off

print("最初の状態      :", "押されている" if button.is_pressed else "離れている",
      "/ LED", "ON" if led.is_lit else "OFF")

# --- ここから「指でボタンを押す」代わりの操作 ---
button.pin.drive_low()   # プルアップなので LOW = 押された
print("ボタンを押した  :", "押されている" if button.is_pressed else "離れている",
      "/ LED", "ON" if led.is_lit else "OFF")

button.pin.drive_high()  # HIGH = 離した
print("ボタンを離した  :", "押されている" if button.is_pressed else "離れている",
      "/ LED", "ON" if led.is_lit else "OFF")

# LEDのピン(GPIO17)の電圧の変化を確認
print("GPIO17の状態の履歴:", [s.state for s in led.pin.states])
実行結果:仮想ピンでボタンを押すとLEDがONになる
実行結果:仮想ピンでボタンを押すとLEDがONになる

実行すると、指でボタンを押す代わりに drive_low() を呼んだ瞬間、when_pressed に登録した led.on が動いてLEDがONになります。実際の結果はこちらです。

LEDのピンの履歴が OFF → ON → OFF(False → True → False) になっていますね。配線もRaspberry Piもなしで、イベントの登録が正しいかを確認できました🚀

ステップ2:人感センサーライトを作る(つまずきポイントあり)

次はもう少し実用的に、人が来たらライトを点け、いなくなったら消すプログラムです。人感センサー(HC-SR501など)は、人を検知すると出力がHIGHになります。gpiozeroでは MotionSensor クラスで扱えます。

後でテストしやすいように、「ライトを制御する仕組み」を関数 setup_motion_light() に分けておくのがポイントです。

import time
from datetime import datetime
from gpiozero import Device, LED, MotionSensor


def setup_motion_light(sensor, light, log):
    """人を検知したらライトON、いなくなったらOFFにして、ログに記録する"""
    def on_motion():
        light.on()
        log.append(f"{datetime.now():%H:%M:%S} 人を検知 → ライトON")

    def on_no_motion():
        light.off()
        log.append(f"{datetime.now():%H:%M:%S} 人がいない → ライトOFF")

    sensor.when_motion = on_motion
    sensor.when_no_motion = on_no_motion


if __name__ == "__main__":
    from gpiozero.pins.mock import MockFactory
    Device.pin_factory = MockFactory()   # ← 実機で動かすときはこの2行を消すだけ

    sensor = MotionSensor(4)   # 人感センサー(HC-SR501など)の出力 → GPIO4
    light = LED(17)            # ライト代わりのLED → GPIO17
    log = []
    setup_motion_light(sensor, light, log)
    sensor.wait_for_no_motion()   # ★センサーが「人がいない」状態を一度読むまで待つ

    # --- センサーの反応をシミュレーション(実機では人が通ると自動でこうなる) ---
    for i in range(2):
        sensor.pin.drive_high()   # センサー出力がHIGH = 人がいる
        time.sleep(0.5)           # センサーは0.1秒ごとに値を読むので少し待つ
        print(f"{i + 1}回目 人がいる   → ライト", "ON" if light.is_lit else "OFF")
        sensor.pin.drive_low()    # LOW = 人がいなくなった
        time.sleep(0.5)
        print(f"{i + 1}回目 人がいない → ライト", "ON" if light.is_lit else "OFF")

    print("--- ログ ---")
    print("\n".join(log))
実行結果:人感センサーライト(wait_for_no_motion追加後)
実行結果:人感センサーライト(wait_for_no_motion追加後)

ポイントをまとめるとこんな感じです。

  • when_motion / when_no_motion に関数を登録すると、センサーの変化で自動的に呼ばれる
  • MotionSensor は裏側のスレッドで0.1秒ごと(sample_rate=10)にピンを読んでいるので、drive_high() のあとに少し待つ
  • ★印の sensor.wait_for_no_motion() が今回のつまずきポイント

実は最初、★の1行がない状態で動かしたところ、1回目の「人がいる」でライトがOFFのままでした。3回実行しても毎回同じです。これ、なぜだか分かりますか?

gpiozeroのソースを確認すると、センサーが最初に読んだ値は「基準の状態」として記録するだけで、イベントは呼ばない作りになっていました。センサーを作った直後にすぐ drive_high() したため、「人がいる」が基準になってしまい、when_motion が呼ばれなかったわけです。

そこで、センサーが一度「人がいない」状態を読むまで wait_for_no_motion() で待ってから動かすようにしました。実機でも、起動直後の人感センサーは出力が安定しないことがあるので、この1行は入れておいて損はありません。修正後の実行結果はこちらです。

1回目からきちんとONになり、ログにも時刻つきで記録されました😊 こういう「実機だと原因が分かりにくい挙動」をPC上で再現して調べられるのも、MockFactoryの大きなメリットです。

ステップ3:pytestで自動テストにする

毎回目で確認するのは大変なので、pytestの自動テストにしましょう。motion_light.py と同じフォルダに test_motion_light.py として保存します。

ここが重要です👇

  • @pytest.fixture で、テストごとに新しい仮想ピンとセンサー・LEDを用意し、終わったら close() で片付ける
  • LEDのピンの履歴を [False, True, False] とリストで丸ごと比較すると、余計なON/OFFが起きていないかまで確認できる
import time
import pytest
from gpiozero import Device, LED, MotionSensor
from gpiozero.pins.mock import MockFactory
from motion_light import setup_motion_light


@pytest.fixture
def devices():
    Device.pin_factory = MockFactory()      # テストごとに新しい偽ピンを用意
    sensor = MotionSensor(4)
    light = LED(17)
    yield sensor, light
    sensor.close()                          # 後片付け(ピンを解放)
    light.close()
    Device.pin_factory.reset()


def test_人を検知したらライトがつく(devices):
    sensor, light = devices
    log = []
    setup_motion_light(sensor, light, log)
    sensor.wait_for_no_motion()

    sensor.pin.drive_high()
    time.sleep(0.3)
    assert light.is_lit
    assert "人を検知" in log[-1]


def test_人がいなくなったらライトが消える(devices):
    sensor, light = devices
    log = []
    setup_motion_light(sensor, light, log)
    sensor.wait_for_no_motion()

    sensor.pin.drive_high()
    time.sleep(0.3)
    sensor.pin.drive_low()
    time.sleep(0.3)
    assert not light.is_lit
    # LEDのピンが OFF → ON → OFF の順に変化したか
    assert [s.state for s in light.pin.states] == [False, True, False]

python -m pytest test_motion_light.py -v で実行すると、筆者の環境では次のように 2 passed になりました。

test_motion_light.py::test_人を検知したらライトがつく PASSED
test_motion_light.py::test_人がいなくなったらライトが消える PASSED
2 passed in 1.16s

ちなみに、試しにテスト側の sensor.wait_for_no_motion() の行を2か所とも消して実行してみたところ、2つのテストがどちらもFAILになりました。ステップ2のつまずきを、テストでちゃんと検出できるわけですね。

実機のRaspberry Piで動かすときは

Raspberry Piが届いたら、motion_light.py の MockFactoryを指定している2行を消すだけで、本物のGPIOを使うようになります。setup_motion_light() の中身は一切変えなくてOKです。

  • シミュレーション用の drive_high() / drive_low() の部分は、実機では pause()(from signal import pause)などに置き換えて待ち続ける形にします
  • テストファイルは実機でもPCでもそのまま動くので、残しておきましょう

まとめ

  • gpiozeroのMockFactoryを使うと、Raspberry PiなしでGPIOのプログラムをPC上で動かせる
  • 入力(ボタン・センサー)は pin.drive_low() / pin.drive_high() で再現し、出力は is_lit や pin.states で確認する
  • センサーは最初に読んだ値ではイベントが呼ばれない。wait_for_no_motion() で落ち着くのを待ってから動かす
  • 制御部分を関数に分けておけば、pytestで自動テストでき、実機では2行消すだけで動く

「配線してから悩む」を「PCで確認してから配線する」に変えるだけで、電子工作のつまずきはぐっと減ります。「むずかしそう」を「できそう」に変える第一歩として、ぜひ試してみてください🚀

📡 Arduinoをもっと深く学ぼう!

Arduino・ラズパイ・ロボットプログラミングを体系的に学びたい方へ。おすすめのUdemyコースや電子部品もまとめています。

Arduinoロボット入門のおすすめコース・部品を見る →

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る
Arduinoをはじめよう 第4版 (Make: PROJECTS)

もしも

Arduinoをはじめよう 第4版 (Make: PROJECTS)

Arduino公式推薦の定番入門書

Amazonで見る
徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

もしも

徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

電子工作とプログラミングを同時に学べる

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。