前回(第2回) でBlazorアプリがPi上で動きました。今回はいよいよハードに触れます。LEDと押しボタンをGPIOにつなぎ、Webの画面からLEDを点灯/消灯し、ボタンの状態を表示します。
Table of contents
Open Table of contents
用意するもの
- LED × 1、抵抗(330Ω程度)× 1(LEDの電流制限用・必須)
- 押しボタン × 1
- ジャンパーワイヤー、ブレッドボード
GPIOの基本操作には
raspi-configでの有効化は不要です(SPI/I2Cは第4回で有効化します)。
配線
ピン番号は BCM(GPIO番号) で指定します(System.Device.Gpio の既定は論理=BCM番号)。
- LED:
GPIO18→ 抵抗(330Ω) → LEDのアノード(+/長い脚) → LEDのカソード(-/短い脚) →GND - ボタン:
GPIO24↔ ボタン ↔GND(内部プルアップを使うので外付け抵抗は不要)- 内部プルアップ=押していないとき High、押すと GND に落ちて Low

System.Device.Gpio を追加
プロジェクト(第2回の TempMonitor)で、.NET IoT の GPIO パッケージを追加します。
dotnet add package System.Device.Gpio
GPIOをサービスにする(DI)
GPIOはアプリ全体で共有するハードウェア資源なので、GpioController をシングルトンのサービスにまとめます。
// Services/GpioService.cs
using System.Device.Gpio;
public sealed class GpioService : IDisposable
{
private const int LedPin = 18;
private const int ButtonPin = 24;
private readonly GpioController _controller = new();
private readonly object _gate = new(); // 複数スレッドからの同時アクセスを直列化
public GpioService()
{
_controller.OpenPin(LedPin, PinMode.Output);
_controller.OpenPin(ButtonPin, PinMode.InputPullUp);
}
public void SetLed(bool on)
{
lock (_gate)
_controller.Write(LedPin, on ? PinValue.High : PinValue.Low);
}
// 内部プルアップのため、押している間だけ Low になる
public bool IsButtonPressed()
{
lock (_gate)
return _controller.Read(ButtonPin) == PinValue.Low;
}
public void Dispose() => _controller.Dispose();
}
Program.cs に登録します。
builder.Services.AddSingleton<GpioService>();
Blazorの画面から操作する
対話操作が必要なので、コンポーネントに @rendermode InteractiveServer を付けます(第1回で選んだ Blazor Server の対話モード)。
@* Components/Pages/Gpio.razor *@
@page "/gpio"
@rendermode InteractiveServer
@implements IDisposable
@inject GpioService GpioSvc
<h1>GPIO操作</h1>
<button class="btn btn-primary" @onclick="ToggleLed">
LEDを@(ledOn ? "消す" : "点ける")
</button>
<p>ボタンの状態:<b>@(pressed ? "押されています" : "離れています")</b></p>
@code {
private bool ledOn;
private bool pressed;
private Timer? _timer;
protected override void OnInitialized()
{
// 200msごとにボタン状態を読み、変化があれば再描画
_timer = new Timer(_ =>
{
var now = GpioSvc.IsButtonPressed();
if (now != pressed)
{
pressed = now;
InvokeAsync(StateHasChanged);
}
}, null, 0, 200);
}
private void ToggleLed()
{
ledOn = !ledOn;
GpioSvc.SetLed(ledOn);
}
public void Dispose() => _timer?.Dispose();
}
ここではボタン状態を簡易的にポーリングしています。よりスマートなリアルタイム更新は第5回(SignalR)で扱います。
実行
必ずRaspberry Pi上で実行します(開発PCにはGPIOが無いため例外になります)。
dotnet run --urls "http://0.0.0.0:5000"
別PCのブラウザで http://<PiのIP>:5000/gpio を開き、ボタンを押すとLEDが点灯/消灯、実機のボタンを押すと画面表示が変われば成功です。

つまずきポイント
- 権限エラー(Permission denied / Access denied):GPIOアクセスにはユーザーが
gpioグループに入っている必要があります。Raspberry Pi OS の既定ユーザーは通常入っていますが、なければsudo usermod -aG gpio $USERの後に再ログイン - 開発PCで実行すると落ちる:
GpioControllerはPiのようなGPIOを持つ環境が前提。実機で動かします - LEDが光らない:LEDの向き(長い脚が+)と抵抗を確認。ピンは物理番号ではなくBCM番号
まとめ
System.Device.GpioのGpioControllerを シングルトンのDIサービスにまとめる- 出力(LED)は
Write、入力(ボタン)はRead。内部プルアップで配線を簡素化 - Blazorは
@rendermode InteractiveServerでWebからハードを操作 - ピンは BCM番号、実行は 実機(Pi)で
次回(第4回)は、いよいよ温度センサー。アナログの LM35DZ を、MCP3208(SPIのADC)経由で読む方法を扱います。
