Dive into Raspberry Pi 2026

はじめに

2026年1月、5回目の改訂に着手しました。Raspberry Pi OSの最新バージョンはTrixieになりました。著者がRaspberry Piに触り始めてから10年以上が経過しましたが、その間、Raspberry Pi自体の性能も大幅に向上し、OSも安定してきたことで、教育用のコンピュータとしてだけでなく、実際の業務用途にも耐えうるプラットフォームになってきたと感じています。本書では、Raspberry Pi OS Trixieをベースに、画像解析やAIなどの最新の技術動向も踏まえた内容を紹介していきます。

2024年版までは古いOS向けの内容も併記するスタイルをとっていましたが、今回からは最新版の内容のみにしてスリム化しています。Bookworm以前の情報が必要な場合は、2024年版(Dive into Raspberry Pi 2024)をご参照ください。

対象読者と前提知識

当初のターゲットが「高校生向け」ということもあり、基本的な部分のみに着目し、なるべく専門用語を避ける、あるいは用語の解説をつけるように心がけています。それでも、例えばコントロールキーと普通のキーを組み合わせてコマンドを実行するなど、コンピュータを扱う上での基礎的なリテラシはある程度必要になってきます。また、インストールを始めとしてほとんどすべての操作は「英語」で行っています。解説通りに進めば英語の知識はそれほど必要ありませんが、本書の内容を応用したり、予期しないエラーに遭遇してしまう場合など、次のステップに進む際には、ある程度の「英語力」は必要になります。著者がそうしてきたように、好きなことをやりながら辞書を片手に(AIやオンライン辞書を横に置いて)一緒に英語も学んでいくスタイルが一石二鳥かと思います。

動作確認環境

本書は、以下の環境を前提に執筆しています(2026年10月時点)。

項目 バージョン
本体・OS Raspberry Pi 5、Raspberry Pi OS(Trixie、64bit)
Python 3.13.5
Ruby 3.3.8(OS標準)、4.0.7(rbenvでインストール。実践課題4・5・7)
主なライブラリ rgpio 0.1.0、prremote 0.4.2、Ruby on Rails 8.1.4、ultralytics 8.4

これより新しいバージョンでも大抵は動きますが、うまくいかない場合はバージョンの違いも疑ってみてください。

用語と基礎知識

本書に登場する主な言葉を解説します。コンピュータの仕組みを解説すると、どうしてもカタカナ(英語)の用語が頻繁に登場することになりがちです。なるべく平易な表現を心がけていますが、実際の現場で使われる語彙とかけ離れてしまうのも問題です。幸い、現在では、Wikipediaなどのインターネット上のリソースも豊富です。意味の分からない単語に出会ったらそのままにせず、Googleなどの検索エンジンで一度調べてみることをおすすめします。著者の私は、この分野で10年以上働いていますが、未だに知らない(新しい)単語にしょっちゅう出会います。「知らない」のが普通です。「どうやって知るか」という方法を身につけることが大切です。コンピュータやインターネットの用語集を手元に持っておくのも良いかもしれません。

Raspberry Pi

Raspberry Pi 5

イギリスで教育用に開発された手のひらサイズの小さなコンピュータ。10000円程度の廉価ながら、ディスプレイの入出力や通信機能など、コンピュータとして最低限の機能を備えています。拡張性も高く、さまざまな応用・工夫を施して利用している人もたくさんいます。公式サイトに利用事例や子供向けの工作課題などが多数掲載されています。2023年の9月には、「Raspberry Pi 5」が発売され、本書で新たに解説する画像解析やAIの実験機としても有用性が見えてきました。

2018年の6月に発売された「Raspberry Pi 3 B+」、小型化を図った「Raspberry Pi Zero」シリーズや「Raspberry Pi 3 A+」、翌年の2019年6月に発売された「Raspberry Pi 4 Model B」なども本書で扱ってきています。各モデルの大きな違いは処理性能ですが、備えている端子の種類がそれぞれ異なっていますので、必要な周辺機器が使えるかはモデルごとに確認が必要です。

Raspberry Pi OS

Linuxディストリビューションの一つである「Debian」をRaspberry Pi用にカスタマイズしたディストリビューションです。OSとしての基本機能のみならず、35000以上のパッケージが用意されており、カメラモジュールのドライバやコマンドも最初から整っており、ScratchやSonic Piなどの教育用のアプリが充実している点も魅力です。本書では、Raspberry Pi OSをインストールしたRaspberry Piをベースに課題に取り組みます。

専門的な用途に使う場合は、Raspberry Pi OS以外にもUbuntu,PidoraやArchLinuxなどさまざまな選択肢が用意されています。

Ruby

Ruby

(Rubyロゴ: Copyright © 2006, Yukihiro Matsumoto. CC BY-SA 2.5)

まつもとゆきひろ氏が開発したプログラミング言語です。ウェブサービスを立ち上げるための言語として有名ですが、その他にもたくさんの機能があって世界中で幅広く使われています。フリーソフトウェアとして配布されています(誰でもお金を払うことなく使えます)。執筆時点での最新バージョンは4.0ですが、Raspberry Pi OSにはそれより少し古いバージョン3.3が提供されています。もちろん新しい方を使うほうがベターなんですが、基本的な用途では多少古いバージョンでも問題なく使えます。

本書の実習では、rbenvという拡張ツールを使って複数のバージョンを切り分けて利用する方法も紹介します。

Python

Pythonの対話画面

オランダのGuido van Rossum氏が開発したプログラミング言語です。読みやすさを重視した言語構造が特徴で、Ruby同様、世界中で広く使われています。さまざまな専門分野のライブラリが充実していることも魅力です。現在のバージョン(3.x)では改善されているものの、過去の2.x系では日本語(マルチバイト文字)の扱いがRubyに比べると煩雑でした。筆者は元々、Pythonを業務に用いてきた経緯がありますが、現在はRubyをメインにしています。Rubyの方が、日本語テキストの処理は容易だったという(日本人ならではの)理由です。

nano

Raspberry Pi OSにデフォルトで搭載されたテキストエディタです。Linuxのテキストエディタといえば、viかemacsが有名ですが、nanoはこれらに比べてシンプルで覚えやすい操作性が特徴です(逆に、viやemacsは習得が困難な代わりに非常に高機能です)。本書ではnanoを使って、各種設定ファイルの編集や、プログラムの記述を行います。本書の後半で基本的な使い方を解説しています。

準備と基本操作

この章では、Raspberry Piを利用するのに必要な周辺機器とインストールから起動までの手順を解説します。Raspberry Piが出始めた頃は日本で入手するのは難しい時期もありましたが、現在はAmazonなどの大手ネットショップでも比較的簡単に見つかるようです。値段等はまちまちなので、複数比較して納得のいくものを選択すると良いでしょう。筆者はRaspberry Pi Shop by KSYや、SWITCH SCIENCEなどをよく使います。

必要な機器

Required items
RaspberryPi 本体(Raspberry Pi 5をベースに解説してゆきます)
USB TypeC - 電源ケーブル(5V5A給電可能なもの)
MicroSDカード(16GB、Class10以上推奨)
ヒートシンク または ファン付属のケース

時々必要になるもの:

液晶ディスプレイ(HDMI端子のついたもの)
HDMI - Micro HDMIケーブル
マウス(USB接続)
キーボード(USB接続)

また写真には載っていませんが、MicroSDカードにOSのインストーラをコピーするために、MicroSDにデータの書き込みができるPC(MacやWindowsなど)も必要です。

最初の設定時にネットワークにつないでしまえば、その後はsshやVNCなどのリモートアクセスで利用することができます。Raspberry Pi自体はそれほど高価なものではありませんが、これら周辺機器をすべて買い揃えていると結構な金額(特にディスプレイ)になってしまうことがありますので、インストールの時だけHDMI接続のテレビなどを利用して後はリモートアクセスで開発を進めるなど、状況に応じて工夫してください。

OSのインストール

公式サイトで配布されているRaspberry Pi Imagerというアプリケーションを利用して、Raspberry Pi OSがインストールされたSDカードを作成する方法をおすすめします。Mac、Windows、Ubuntu版が用意されています。

以前はddコマンドを使ってSDカードにOSのイメージをコピーしていましたが、引数を間違えると事故につながるため、極力上記のインストーラを利用してください。

インストールする際に、ホスト名やユーザ名を任意に設定することが可能です。本書の解説では、ホスト名をraspi5、ユーザをpiuserと設定したイメージを利用しています。ご自身の設定内容に合わせて、適宜読み替えてください。

初回起動と基本設定

MicroSDカードをRaspberry Pi本体に挿入し、電源ケーブルを接続します。初回起動時には、画面の指示に従って、言語やタイムゾーン、キーボードの選択、Wi-Fiの設定などを行います。

リモート接続

左上のメニューから Preferences -> Control Centre と進み Interfaceのタブを選択します。コマンドラインからは以下のコマンドで起動可能です(見た目は異なります)。

sudo raspi-config
Raspberry Pi Configuration

必要なインタフェース(接続手段)をEnableに変更してください。これらはRaspberry Piを遠隔から扱う上で便利なものですが、一方でセキュリティ上のリスクを増大させるものでもあります。パスワードを強固なものにしたりするなどの対策も合わせて検討が必要です。

また、この他にRaspberry Pi Connectというツールが付属するようになりました。初回にアカウント作成が必要ですが、これを利用すると同じローカルネットワークからのみならず、インターネット側からでも気軽に接続ができるようになります。

Raspberry Piを複数台使用する場合、名前(普通にインストールするとraspberryという名前がついています)が重複するとうまく接続できなくなりますので、重複のない名前に変更する必要があります。同じ設定ツールの中の「System」タブにある「Hostname」という項目から変更してください。

現在のRaspberry Pi OS(Trixie)のVNCは、ディスプレイを接続していない状態でも動作するため、特別な設定をしなくてもリモートから利用できます。

ssh接続はお使いの端末から以下のように利用可能です(Macの場合)。

ssh piuser@raspi5.local

このポートを使って、ファイルのコピー(scp,rsync)なども可能です。VNCや後述するファイル共有サーバ(samba)でも、ファイル転送は可能ですが、これらのコマンドを使うことで”自動化”が容易になります。

ソフトウェアの更新

新しいパッケージを追加する際などに、既存のOSが最新であることを求められる場合があります。以下のコマンドで、OSを更新することができます。

sudo apt update && sudo apt upgrade -y

完了後に再起動が必要な場合もあります。

sudo reboot

セキュリティ上の問題が修正されたりするケースもありますので、時々更新する習慣をつけておくとよいかと思います。

デスクトップを利用している場合は、更新が必要な時には右上に下向き矢印のアイコンが表示されます。これをクリックして実行することも可能です。

ターミナルの使い方

画面の左上にある黒っぽいアイコンをクリックするか、またはCtrl+Alt+Tを押して起動します。黒いウィンドウの左上に以下のような表示が出てくれば正常に起動できています。

piuser@raspi5:~ $

この表示をプロンプトと呼びます。Raspberry Pi OSを始め、Linux系のOSでは、ここからさまざまな命令をコンピュータに対して送ることができます。MacやWindowsにも似たような画面が存在します。MacのコマンドはLinuxととてもよく似ていますが、Windowsだけはかなり違った言語を使います。

$のマークの後ろにカーソルがあることを確認したら、以下の命令を打ってみましょう。文字を打ち終わったらEnterキーを押します。

ls

lsは今いる場所のファイル及びディレクトリを一覧表示してくれるコマンドです。以下のように (ファイルやディレクトリの)名前がいくつか表示され、また元のプロンプトが表示されていることを確認してください。

piuser@raspi5:~ $ ls
Documents  Pictures  Public..
(略)
piuser@raspi5:~ $

このターミナルを主に使って、さまざまな設定やプログラムを書いていきます。

nanoエディタの使い方

本書では設定ファイルやプログラムの編集に「nano」エディタを使用しています。ここでは基本的な使用方法を紹介します。さらに詳しい使い方はnanoのヘルプページ(起動後に「^G」で表示)や公式サイトを参照してください。

起動は「nano」コマンドに続けて編集したいファイル名を指定します。新規作成の場合も同様です(保存時に指定した名前のファイルが生成されます)。

nano sample.txt

基本的な操作は、おそらくWindowsやMacを使ったことがあれば問題なく行えるのではないかと思います。カーソルキーで移動して、文字をタイプ、Backspaceキーで削除したりReturnキーで改行、などは一般的なテキストエディタと同じです。画面の下部に主要な操作が列挙してあります。白地に黒抜きで表示してある部分が、その操作を実行するためのキーです。「^G」は「Ctrl」キーを押しながら「G」キーを押す、という意味になります。

nano menu

以下の操作を覚えていれば本書で扱うくらいのプログラムの記述には十分です。

「^O」 - ファイルを保存します。
「^K」 - カーソルがある行をカットします。
「^U」 - カットした行をペーストします。
「^X」 - 終了します。未保存のファイルがある場合は、保存するか確認されます。

シンタックスハイライトや検索の機能もあって、初心者でもそこそこ使える便利なエディタという感じですが、本格的にプログラミングを学びたいという場合には、IDE(例えばNetbeansやEclipse)やVSCodeなどの後発のエディタ利用も選択肢に入れても良いかもしれません。Raspberry Pi OSには、ThonnyとGeanyという簡易的なIDEも標準でインストールされています。VimやEmacsといった歴史のあるエディタも使いこなせば強力です。

本書で使うプログラミング言語

本書ではPythonとRubyの2つの言語を使います。画像処理・AI(実践課題6・7)や、付録のセンサの課題のように、Pythonのライブラリが充実している分野ではPythonを使います。GPIOを使った電子工作(実践課題3)、Pico W(実践課題4)、ウェブアプリ(実践課題5・7)では、筆者が普段の仕事で使っているRubyを使います。どちらの言語も、基本的な書き方を以降の節で紹介します。

Pythonの基本

GPIOやカメラなどRaspberry Piから外部のデバイスを操作するライブラリの多くはPythonという言語で提供されています。他の言語でも扱えないことはないのですが、インターネット上に情報が多く、安心して利用できるメリットがあります。そもそもPython自体が、とても覚えやすいシンプルな言語ですので、Raspberry Piを使いこなそう!と思ったら必ずと言って良いほど触れる機会が出てくると思います。ここでは、基本的な構文についていくつか紹介します。

まずは、Pythonを起動する方法と、画面に文字列「Hello, world!」を表示する方法です。

$ python
Python 3.13.5 (main, ...) on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("Hello world!")
Hello world!

PythonはWindowsやMacでも広く使われているメジャーな言語です。もちろん、Raspberry Pi OS上でも、同じようにPythonを起動することができます。以降のサンプルはどの環境でも同じように動作します。

「>>>(プロンプトと呼びます)」の記号の後ろに、Pythonに対する命令を入力して実行することができます。まず、以下のように数式を打ってみましょう。プロンプトの後ろを実際にタイプします。その次の行に表示されているのが実行結果です。

>>> 1 + 3
4
>>> 5 * 20 / 4
25.0

ちょっとした電卓がわりにも使うことができそうですね。「*(アスタリスク)」は掛け算、「/(スラッシュ)」は割り算の記号として使われます。

Pythonを終了するときは「Ctrl+D(Ctrlキーを押しながらD)」を入力します。

ちょっとした命令なら、上記のように起動、入力(実行)という手作業でも問題ありませんが、少し複雑なことをしたい場合は、プログラム(命令)をあらかじめファイルに書いておいて、それを実行するという方法を取ることになります。Pythonのプログラムが書かれたファイル(スクリプト、などと呼びます)の名前を現在のディレクトリに置いて、以下のように指定します。

python foo.py

スクリプトの作成と保存は、前述のnanoエディタなどを使うと便利です。Raspberry Pi OSで利用できるエディタは他にも色々ありますので、ぜひ調べてみてください。

以降の実例に出てくるように、Pythonの特徴の一つに「インデント(行頭の空白またはタブ)による構文の表現」が挙げられます。例えば、以下の条件分岐の例は、「vが10より大きければ、“v is bigger than 10.” と表示、vが10と等しければ、”v equals 10.”、それ以外ならば、“v is smaller than 10.” と表示」するプログラムです(これを実行すると何と表示されるか、分かりますか?)。「if .. elif .. else」のそれぞれに合致した時に実行されるブロック(行のかたまり)は行頭に4つの空白がある部分、として定義されます。言葉で説明すると、非常にややこしいですが、見慣れてくると(括弧を多用する他の言語と比べて)シンプルで分かりやすく感じられると思います。

v = 12

if v > 10:
    print("v is bigger than 10.")
elif v == 10:
    print("v equals 10.")
else:
    print("v is smaller than 10.")

条件分岐の他に、「繰り返し」もプログラム中でよく使う構文です。forとwhileの2つのキーワードがあります。forは「0から9まで(10回)繰り返す」というように回数が定まっている場合に便利です。

for i in range(0,10):
    print(i)

一方で、whileは「oooのあいだ」というように、ある条件が成立している(いない)間はずっと繰り返す、という場合に便利です。

i = 0
while i < 10:
    print(i)
    i += 1

関数

ある処理のまとまりを関数として記述しておくと、後からそれを呼び出して何度も使うことができます。 以下の例ではhelloという名前の関数を定義し、その後、”John”、”Cat” という引数を渡して呼び出しています。引数とは関数の呼び出し時に渡す値のことで、「いんすう」ではなく「ひきすう」と読みます。

def hello(name):
    print("Hello, %s." % (name))

hello("John")
hello("Cat")

このプログラムを実行すると、以下のように表示されます。

Hello, John.
Hello, Cat.

インポート(import)

モジュールのインポートは本書でも何度か登場します。Minecraftに接続する、GPIOのピンを介した通信をする、など外部とやりとりをしたり、プログラミング言語の基本機能として登場している以外のことをする場合に利用します(要するに現実のプログラミングではほとんど必ず使います!)。

例えば、「os」という名前のモジュールを使用する(=インポートする)ときは以下のように記述します。

import os

インポートされた後は、以下のようにして「os」モジュールが提供する関数やクラスを利用することができるようになります。以下のコードは、現在のディレクトリに存在するファイルの一覧を配列で返します。

os.listdir(".")

「os」モジュールには、オペレーティングシステムとやり取りをする際に必要な関数が多数用意されています。主にファイルの操作をする時に利用します。このように、プログラムの中で必要に応じてさまざまなモジュールをインポートしながら、必要な処理を実装して行きます。あらかじめマシンにインストールされている以外にもたくさんのモジュールが公開されており、これらをインターネット経由で導入・利用することも可能です。

配列

配列とは、オブジェクト(数値や文字など)を連続して格納することができるデータ型です。 以下のように、定義したい値を[](角括弧、ブラケット)で囲んで、コンマで区切ることで定義ができます。

a = [1,2,3]

他にも、範囲を指定して以下のように定義することができます。 rangeで1から4未満の整数の範囲を定義し、それをlistで配列に変換します。

a = list(range(1,4))

print関数で中身を表示することもできます。

>>> print(a)
[1,2,3]

配列の中の個々のデータのことを配列の「要素」と呼びます。 要素は後から追加したり削除したりすることができます。

del(a[0])  # => [2,3]
a += [4]   # => [2,3,4]

他にもたくさんの配列の操作用の関数や演算子が存在しています。 また、Pythonでは、配列に似た「タプル」というデータ型も存在します。 これは()(括弧、パレンシス)で囲んで定義します。多くの場面で配列と同じように振る舞いますが、 大きな違いとしてタプルは一度定義すると後から要素を変更できない、という点があります。

a = (1,2,3)

辞書

キーと値のペアからなるデータ構造をPythonではdictionary(辞書)と呼びます。他の言語では連想配列やハッシュと呼ばれたりもします。以下のように宣言します。

d = {"a":1, "b":2}

以下のように値を参照することができます。

d["a"] #=> 1
len(d) #=> 2
d.keys() #=> ['a', 'b']
d.values() #=> [1, 2]

要素を削除する場合は、配列と同様に del を利用します。

del(d['a'])

制御文字を利用する

PythonにはGUI(グラフィカルなユーザインタフェース)を操作できるライブラリもたくさん揃っていますが、それらが使えない環境も存在します。そういう場合にでも、ちょっとしたビジュアライズを行いたい時に便利な制御コードがあります。これを使って、簡単なグラフを描くサンプルです。

import random, sys, time

color = 91  # 90:gray, 91:red , 92:green, 93:yellow, 94:blue, 95:magenta

while True:
  w = 60
  i = int(random.random() * w)
  sys.stderr.write("\033[%dm" % (color))
  sys.stderr.write('\r' + ('o' * i) + ' ' * (w-i))
  sys.stderr.write("\033[0m")
  sys.stderr.write(' ' + ('%02d' % (i)))
  sys.stderr.flush()
  time.sleep(0.3)
% python3 graph.py
oooooooooooooooooooooooooooooooooooo                         36

終了は「Ctrl+C」です。

コマンドライン引数を受け取る

ある程度複雑なプログラムを書けるようになってくると、状況に応じてプログラムの動作を切り替えたくなることがあります。そんな時に便利なのが、argparseモジュールです。これを使うと、一般的なLinuxコマンドが持つような各種のコマンドライン引数を自分のPythonプログラムでも受け取ることができるようになります。

import argparse

parser = argparse.ArgumentParser(description='Argparse sample')
parser.add_argument('-f','--flag',action='store_true',help='Set flag')

args = parser.parse_args()

print(args)

上記のプログラムがarg.pyという名前で保存されている場合、以下のように動作します。

% python3 arg.py       
Namespace(flag=False)

-fまたは--flagというオプションをコマンド名の後に付けると、フラグの値が変わります(store_trueというアクションの動作です)。

% python3 arg.py -f
Namespace(flag=True)

また、argparseを使うと自動で-hオプションが使えるようになります。これを指定するとコマンドの利用方法が画面に表示されます。

% python3 arg.py -h
usage: arg.py [-h] [-f]

Argparse sample

options:
  -h, --help  show this help message and exit
  -f, --flag  Set flag

おまけ:ひとつ手前のグラフのプログラムと組み合わせて、色と記号をコマンドラインから指定可能にしてみました。

import random, sys, time
import argparse

parser = argparse.ArgumentParser(description='Bargraph')
parser.add_argument('-c','--color',default=92)
parser.add_argument('-s','--symbol',default="#")

args = parser.parse_args()

color = int(args.color)  # 90:gray, 91:red , 92:green, 93:yellow, 94:blue, 95:magenta

while True:
  w = 60
  i = int(random.random() * w)
  sys.stderr.write("\033[%dm" % (color))
  sys.stderr.write('\r' + (args.symbol * i) + ' ' * (w-i))
  sys.stderr.write("\033[0m")
  sys.stderr.write(' ' + ('%02d' % (i)))
  sys.stderr.flush()
  time.sleep(0.3)

Rubyの基本

Raspberry Piでruby言語を使うためには、まず以下のコマンドでインストールが必要です。

sudo apt install ruby

-vオプションをつけて起動して、バージョンを確認します。

$ ruby -v
ruby 3.3.8 (2025-04-09 revision b200bad6cd) [aarch64-linux-gnu]

最新の機能も導入したい場合は、実践課題5などで紹介しているrbenvなどを利用して別途インストールをしてください。

(この章の以降の解説は別著Ruby入門からの抜粋です)

rubyの準備ができたら、まずは一行だけ、hello.rbに以下のように書き込んで保存してください。

puts "Hello, Ruby!"

保存したら、以下のように入力します。

ruby hello.rb

プロンプトの下の行に、

Hello, Ruby!

と表示されたらOKです!ようこそ、Rubyの世界へ!なにかややこしいメッセージが表示されていたりしたら、前述した手順をトレースする過程で、どこかに抜けや間違いがあるはずです。もう一度慎重に見なおしてみてください。

Rubyには対話型のインタプリタも存在します。irb(Interactive RUbyの略)という名前のコマンドです。以下のように利用します。

$ irb
irb(main):001:0> 1 + 2 + 3
=> 6
irb(main):002:0> puts "Hello, Ruby!"
Hello, Ruby!
=> nil
irb(main):003:0> exit

先ほどはhello.rbにプログラムを書いて保存して、それをrubyコマンドで実行という手順をとりましたが、irbを使うと、プロンプト(>)に直接Rubyのプログラムを書いて実行することができます。小さなコードをその場で実行したり、動作確認をする際に役に立ちますし、計算機代わりに使うことも可能です。irbを終了する場合は「exit」とタイプしてください。

制御構文

if文では「もし〜ならば」という条件分岐を記述することができます。条件が成立しなかった場合の処理はelse句に記述します。最初の n = 10 は変数の宣言部分です。ここでは「nという名前の変数(値を入れる箱)に10という値を入れる」という動作をします。

n = 10
if n == 10
  puts “n is 10.”
else
  puts “n is not 10.”
end

unless文はifと逆で、条件が成立しなかった場合に直後のステートメントが実行されます。

unless n == 10
  puts “n is not 10.”
else
  puts “n is 10.”
end

この構文はRuby以外の言語にはあまり存在しないように思います。ifに否定をつければ同じ動作をするので、冗長な印象を持つこともあるのですが、例えば下記のようにRubyでは実行するステートメントの後部にifやunless付与することが可能であり、ifとunlessをうまく使い分けると、英語の文章のような自然な記述が可能になります。

return unless str.empty?

繰り返し

while文では「ある条件が成立する間」の繰り返し処理を記述することができます。下記のコードでは、nという変数の値が10未満の間、ブロック内のコードが繰り返し実行されます(0から9までの値が画面に表示されます)。「n += 1」を書き忘れると、永遠に条件が成立し続けることになり、いわゆる「無限ループ」にはまってしまいます。while文に限らず、なんらかの理由で処理が終了しなくなった場合は、「Ctrl+C」で強制終了させることができます。

n = 0
while n < 10
  puts n
  n += 1
end

if・unlessと同様に、whileと逆の意味を持つuntil文も存在します。こちらは「ある条件が成立するまでの間」の繰り返し処理になります。上記のコードをuntilを使って書き直すと以下のようになります。

n = 0
until n == 10
  puts n
  n += 1
end

繰り返しの回数があらかじめ分かっている場合は、timesメソッドが役に立ちます。以下のコードも(上記のものと同様)、0から9までの数値を表示するものです。ブロック内変数の「i」には、繰り返しの回数が0から順番に渡されてきます。

10.times do |i|
  puts i
end

範囲オブジェクト(0…10)を使って表現することもできます。

(0...10).each do |i|
  puts i
end

配列で記述すると以下のようになります。列挙可能なオブジェクトのそれぞれの要素に対して何らかの処理を記述するケースは多く、この構文をスムーズに記述・理解できるようになるとRuby言語の習得度は一気に上がるのではないかと思います(ここにブロック引数という重要な概念も含まれています)。

[0,1,2,3,4,5,6,7,8,9].each do |i|
  puts i
end

配列とハッシュ

配列はさまざまなオブジェクトが順序付きで並んだ変数のことです。以下のように[]を使って定義します。

arr = [1,2,3]
arr.count #=> 3
arr.sum #=> 6

ハッシュは「キー:値」のペアの集合です。以下のように{}を使って定義します。

hash = {a:1, b:2, c:3}
hash.keys #=> [:a,:b,:c]
hash.values #=> [1,2,3]

乗算の演算子(*)を使っての配列の宣言は便利ですが、多次元配列にするときには気をつけるポイントがあります。下記のコード例のような動きをします。外側の配列に包含される配列の実体が一つなんですね。気をつけておかないと、思わぬ不具合につながってしまいそうです(公式のドキュメントでも繰り返し明言されています)。

arr = [[0] * 2] * 3
arr[1][1] = 1
puts arr
#=> [[0, 1], [0, 1], [0, 1]]

上記の例では一箇所だけ「1」に書き換えたかったのですが、そのような配列を宣言する場合は以下のように記述します。newの一つめの引数は配列のサイズで二つ目が各要素の初期値になります。初期値を設定する引数にブロック({ }で囲まれた中)も使えるようになっているところが肝ですね。

arr = Array.new(3) { [0] * 2 }
arr[1][1] = 1
puts arr
#=> [[0, 0], [0, 1], [0, 0]]

コマンドライン引数(ARGV)

プログラムを起動するときに、ファイル名の後ろに空白で区切って書き足した文字列を「コマンドライン引数」と呼びます。Rubyでは、これらがARGVという名前の配列に自動的に格納されます。プログラムを書き換えることなく、起動時に動作を切り替えたい場合に便利です。以下の内容をargs.rbとして保存してください。

puts ARGV.size
ARGV.each do |arg|
  puts arg
end

引数を二つ付けて実行すると、引数の個数と、それぞれの中身が表示されます。

$ ruby args.rb apple 123
2
apple
123

ARGVは普通の配列ですので、最初の引数はARGV[0]、二番目はARGV[1]のように取り出せます。利用する際には、以下の点に気をつけてください。

練習問題

日付処理とコマンドライン引数の利用例として、指定した年月のカレンダーを表示するプログラムを書いてみましょう。cal.rbとして保存してください。

require 'date'

t = Time.now
year = t.year
month = t.month

if ARGV.size == 1
  year, month = ARGV[0].split('.').map(&:to_i)
end

print "#{year}.#{month}\n"
print " M  T  W  T  F  S  S\n"

Date.new(year, month, -1).day.times do |i|
  t = Date.new(year, month, i + 1)
  if i == 0
    offset = t.wday > 0 ? t.wday - 1 : 6
    print "   " * offset
  end
  print "%2d " % (i + 1)
  if (t.wday) % 7 == 0
    print "\n"
  end
end
print "\n"

引数を付けずに起動すると今月のカレンダーが表示され、「年.月」の形式で引数を付けると、その月のカレンダーが表示されます。

$ ruby cal.rb 2026.10
2026.10
 M  T  W  T  F  S  S
          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

ARGVを扱っているのは、if ARGV.size == 1から始まる三行です。引数がちょうど一つ指定された場合にだけ、あらかじめ今日の日付から設定しておいたyearとmonthを上書きしています。ARGV[0]は”2026.10”という文字列ですので、split('.')でピリオドの位置で分割して[“2026”, “10”]という配列にし、さらにmap(&:to_i)でそれぞれの要素を整数に変換して[2026, 10]としたうえで、yearとmonthに一つずつ代入しています。

さらに学ぶには

ここで紹介したのはRubyのごく一部です。より詳しく知りたい場合は、まず公式サイト(ruby-lang.org)を訪ねてみてください。最新バージョンのお知らせやインストール方法のほか、入門者向けのチュートリアル、すべてのクラスやメソッドを調べられるリファレンスマニュアルなどが、日本語で公開されています。

公式サイトから辿ることができる「Try Ruby」(try.ruby-lang.org)もおすすめです。ブラウザの中でRubyのコードを実際に動かしながら、基本的な文法を順番に学べる対話型のチュートリアルで、インストールなどの準備は一切必要ありません。Raspberry Piが手元にないときでも、パソコンやタブレットのブラウザから気軽にRubyを試すことができます。

Gitの基本

Gitは現在、おそらく世界で最もメジャーなバージョン管理システムです。GitHub、GitLab、Bitbucketなどさまざまなソースコードのホスティングサービスが存在しますが、それらの共通の基盤になっています。

Raspberry Piでもgitを使うことができます。初めて使う場合は、まず自分の名前とメールアドレスを登録しておきます。コミットには「誰が変更したか」が記録されるため、これを設定しないままgit commitを実行するとエラーになります。名前とメールアドレスは、ご自身のものに置き換えてください。

git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main

--globalを付けると、このユーザで作るすべてのリポジトリに設定が適用されます(一度設定すれば、次回からは不要です)。3行目は、リポジトリを作ったときに最初にできるブランチの名前をmainにする設定です(ブランチについては後述します)。設定した内容はgit config --listで確認できます。

準備ができたら、以下の手順で、手元に一つリポジトリを作ってみましょう。sample.txtというファイル(中身は何でもOKです)を作ってそれをコミット(リポジトリに登録)しています。

mkdir myrepo
cd myrepo
nano sample.txt
git init
git add sample.txt
git commit -m "Initial commit"

この状態でもう一度、sample.txtを編集して何か書き足したり変更してから以下のコマンドを実行してみてください。

git status
git diff

変更が行われたことを確認できたでしょうか?

この変更をリポジトリに登録するには、以下のようなコマンドを利用します。

git add sample.txt
git commit -m "Second commit"

ここまでの変更内容を確認するには以下のコマンドを実行します。

git log

変更を取り消す

コミットしていない編集を取り消して、最後にコミットした状態に戻すにはgit restoreを使います。sample.txtを適当に書き換えてから、以下のコマンドを実行してみてください。

git restore sample.txt

sample.txtの中身が、最後にコミットした時点の内容に戻っているはずです。取り消した編集は元に戻せないので注意してください。なお、git addまで済ませてしまった変更は、git restore --staged sample.txtでgit addする前の状態に戻せます。

管理しないファイルを指定する(.gitignore)

プログラムが出力するログやデータ、venvのように後から作り直せるものは、リポジトリに含めないのが一般的です。リポジトリの直下に.gitignoreというファイルを作り、管理しないファイルやディレクトリを1行ずつ書きます。

nano .gitignore

例えば、以下のように書くと、拡張子が.logと.csvのファイル、およびvenvディレクトリが管理の対象外になります。

*.log
*.csv
venv/

git statusを実行して、ここに書いたファイルが一覧に表示されなくなることを確認してみてください。.gitignore自体はコミットしておきます。

git add .gitignore
git commit -m "Add .gitignore"

ブランチを使う

ブランチは、作業の流れを枝分かれさせる仕組みです。メインの流れ(main)を壊さずに別のブランチで新しいことを試し、うまくいったらmainに取り込む、という使い方をします。

git switch -c experiment
nano sample.txt
git add sample.txt
git commit -m "Try something"
git switch main
git merge experiment

git switch -cで新しいブランチ(ここではexperiment)を作って切り替え、そこで変更をコミットしています。git switch mainでmainに戻った直後は、sample.txtがexperimentでの変更前の内容になっていることを確認してみてください。最後のgit mergeで、experimentでの変更をmainに取り込みます。現在あるブランチの一覧はgit branchで確認できます。

他にもさまざまなサブコマンドが用意されています。

git show ...
git checkout ...
git switch ...

このようにソースコードの変更を記録し、いつでも参照したり巻き戻したりできることは、ある程度以上の規模のプロジェクトでは必須とも言える要件になってきます。個人のプロジェクトでも長期に渡ってメンテナンスする場合にはとても有用です。

参考: Git公式サイト

実践課題1 カメラとサーバ

Raspberry Piにカメラをつないで写真を撮ったり、ウェブサーバやメールサーバを立てたりしてみましょう。ターミナルの基本的な操作も合わせて学びながら、Raspberry Piを使いこなすための第一歩を踏み出してみましょう。

カメラモジュールで写真を撮る

カメラ専用の接続端子に接続するカメラモジュールが必要です。接続済みの状態で、以下のコマンドが利用できるか確認してみましょう。

rpicam-hello

静止画を撮るには、rpicam-still コマンドを利用します。「-o」というオプションに続けて出力ファイル名(ここではpic2.jpg)を指定しています。

rpicam-still -o pic2.jpg

コマンドの後ろに続く「ハイフン+アルファベット」の組み合わせはオプション、またはスイッチと呼ばれるもので、「-o」の他にもさまざまなものが用意されています。例えば「-t」は起動してから撮影するまでの待ち時間をミリ秒で指定できます(指定しない場合は5秒)。「–vflip」「–hflip」はそれぞれ、垂直反転、水平反転のオプションです。カメラの設置向きによって画像が反転してしまう場合がありますが、そういった際に向きを調節するのに便利な機能です。

rpicam-still -t 1 --vflip --hflip -o pic3.jpg

動画の記録には「rpicam-vid」というコマンドを使用します。こちらもrpicam-stillと同じように「-o」に続けて出力ファイル名(vid.mp4)を指定しています。

rpicam-vid -o vid.mp4

その他「–vflip」や「–hflip」も同様に利用できます。「-t」はrpicam-stillの場合、撮影するまでの時間でしたが、rpicam-vidでは、動画を撮影する時間(ミリ秒)を指定します。つまり、以下のコマンドでは1分間の長さの動画が保存されることになります。

rpicam-vid -o vid.mp4 -t 60000

保存された動画はmp4コーデック(ここは著者も詳しくないので割愛します)に対応したプレイヤがあれば、WindowsやMacでも再生可能です。もちろん、Raspberry Piでも再生可能です。

vlc vid.mp4

USB接続のカメラで写真を撮る

一般的なUSB接続のカメラも利用可能です(配線や設置はこちらの方が簡単かもしれません)。fswebcamというコマンドが利用できます。

sudo apt install fswebcam

以下のように保存先のファイルを指定して画像を取得することができます。

fswebcam ~/Desktop/test.jpg

別の課題で細かく触れますが、pythonのスクリプトからもアクセス可能です。OpenCVというライブラリをインストールします。

sudo apt install python3-opencv

以下は、取得した画像を縦横半分のサイズで出力する例です。画像解析やAIでの分類にはこちらの方法が役立ちます。

import cv2

cv2.namedWindow('cv2-captured-image')
cam = cv2.VideoCapture(0)

while True:
    ret, image = cam.read()
    image = cv2.resize(image,None,fx=0.5,fy=0.5)
    cv2.putText(image,"Press any key to exit",(0,24),cv2.FONT_HERSHEY_SIMPLEX,1.0,(255,0,0))
    cv2.imshow('cv2-captured-image', image)
    if cv2.waitKey(10) != -1:
        break
cam.release()
cv2.destroyAllWindows()

ウェブサーバ(Apache)をインストール

Raspberry Piにウェブサーバをインストールしてみましょう。ウェブサーバとは、ウェブサイトを構成する文章(HTML)や画像などのデータを要求のあったクライアント(ブラウザ)に対して送信するためのプログラムです。ここではApacheというウェブサーバをインストールしています。最近だとnginxなども流行(?)っていたり、目的に応じてさまざまなタイプのウェブサーバが存在しています。以下のコマンドでは、「apache2」というパッケージをインストールしています。

sudo apt install apache2

実はたったこれだけでお終いです。手元のRaspberry Piが確かにサーバになっていることを確認するために、以下のようにしてindex.htmlを適当に書き換えてみてください(htmlの説明は割愛します)。

sudo nano /var/www/html/index.html

ファイルの保存が済んだら、ウェブブラウザを立ち上げ(Raspberry Pi上でも、同一ネットワーク内の別のマシンでもどちらでも構いません)以下のアドレスにアクセスしてみましょう。アドレス中の「raspi5」の部分はホスト名にあたります。リモート接続の設定時にホスト名を変更している場合は、その名前に読み替えてください(.localは固定です)。

http://raspi5.local/

以下のようなページが表示されます。(今回はbodyタグの直後にHello world!というメッセージを追加して変更が反映されることを確認しています)

apache2

このまま立ち上げておけば、社内や家庭内向けのウェブページや掲示板などの仕組みを運用することができますね。世界に向けて公開する場合は、グローバルIPアドレスを取得したり、ドメイン名をつけたり、盗聴対策に暗号化の仕組みを取り入れたりと、やるべき設定は多いのですが、もちろん不可能ではありません。

メールサーバを立てる

ウェブサーバの次はメールの送受信を行うメールサーバを導入してみましょう。本格的なメールサーバの運用も可能なのですが、かなり複雑なテーマになりつつも、実運用ではAWS SESやSendGridなどのメール送信サービスを利用するケースが多いと思いますので、ここではローカル(今のraspi上)でのみメールの送信を行うシンプルなメールサーバを構築してみましょう。インストールは先ほどと同様、一行でお終いです。

sudo apt install postfix mailutils

インストールの途中で、メールサーバのタイプを選択する画面が表示されます。ここでは「Local only」を選択してください。

以下のコマンドで送信テストをしてみましょう。最後尾のメールアドレスは、あなた自身のアドレスに書き換えてください。echo の後ろが本文に、 -s の後ろが表題としてメールが送信されます。

echo hi | mail -s 'test mail' piuser@raspi5.local

続けて、以下のコマンドでメールの受信を確認してみましょう。先ほど送ったメールが届いているはずです。

mail 

もしうまく届かない場合は、Local onlyの設定(/etc/postfix/main.cf)が正しくできているか、メールアドレスの指定が正しいかなどを確認してみてください。

合わせ技にチャレンジ

写真を撮って、ウェブサーバで公開して、公開完了をメールでお知らせするプログラムを作ってみましょう。基本的には前述の三つのプログラムを組み合わせるだけですが、Ruby言語を使って少し工夫を取り入れてみましょう。

まずはウェブサーバの準備です。「/var/www/html」がウェブに公開されるファイルを置く場所です。この下に「pictures」という名前のディレクトリを作成し、piuserユーザ(現在ログインしている一般ユーザ)からアクセス(書き込み)可能な状態にします。

sudo mkdir /var/www/html/pictures
sudo chown piuser: /var/www/html/pictures/

プログラム本体を作成します。nanoエディタで「take-pictures.rb」というファイルを編集(新規作成)してください。「.rb」はRubyでプログラムが記述されたファイルに使われる拡張子です。

nano take-pictures.rb

中身は以下のようになります。ちょっと記号が多く暗号めいた雰囲気もありますが…。

12.times do |i|
  f = "/var/www/html/pictures/%03i.jpg" % i
  `fswebcam #{f}`
  sleep(1)
end
puts "See http://"+`hostname`.strip+".local/pictures/" 

以下のコマンドで実行します。

$ ruby take-pictures.rb

正しく起動すると、一秒間に1回ずつ写真を撮って、000.jpgから011.jpgまで連番の形式で合計12枚画像を保存します。撮れた写真はウェブサーバで公開されています。最後に表示されるURLをブラウザで開いて確認してください(以下は一例です、ホスト名の設定によって異なります)。

http://raspi5.local/pictures/

apache2

上記のようにファイルの一覧が表示されていたら成功です。画像名をクリックすると、撮影した画像を確認できます。ウェブアプリケーションを作って、スライドショーで見せたりサムネイルで一覧表示したりと使いやすくするアイデアはたくさん出てきそうです。また、今回は直接コマンドを実行しましたが、この実行のタイミングを一定時間おき、あるいは決まった時刻に、など設定することも簡単です。後述するGPIOを使って各種センサと組み合わせるのも面白いかもしれません。扉が開いたら、人(生き物)が通ったら撮影する、なんてことも実現できます。

以下は画像を4列のグリッドで一覧表示する例です。

<!DOCTYPE html>
<html>
<head>
<style>
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.grid img { width: 100%; height: auto; }
</style>
</head>
<body>
<div class="grid">
<img src="000.jpg">
<img src="001.jpg">
<img src="002.jpg">
<img src="003.jpg">
<img src="004.jpg">
<img src="005.jpg">
<img src="006.jpg">
<img src="007.jpg">
<img src="008.jpg">
<img src="009.jpg">
<img src="010.jpg">
<img src="011.jpg">
</div>
</body>
</html>

ファイル共有サーバとして利用する(samba)

sambaというサービスを使うことで、Finder(Windowsの場合エクスプローラー)からファイルを操作できるようになります。

finder

まずはパッケージをインストールします。

sudo apt install samba

設定ファイルの末尾に、公開するフォルダの情報を追記します。

sudo nano /etc/samba/smb.conf

ユーザのHOMEディレクトリ全体を書き込み可能な形で公開しています。

[piuser]
  path = /home/piuser
  browseable = yes
  read only = no

接続するユーザのパスワードを設定します。

sudo smbpasswd -a piuser

設定を反映させるためにデーモンを再起動します。

sudo systemctl restart smbd

実際に運用する場合には、バックアップやセキュリティの問題を考慮する必要がありますので、導入の検討は慎重に行ってください。

実践課題2 アプリで遊ぶ

Raspberry Piのデスクトップで使えるアプリをいくつか紹介します。プログラミング学習用のScratch、音楽を作るSonic Pi、ゲームのLuanti、画像を編集するGIMPやInkscapeなど、パソコンとしての使い方を試してみましょう。最後に、手元で動く生成AI(LLM)も試します。

Scratchでプログラミング体験

「Scratch」は教育用のプログラミング環境です。ブラウザから以下のアドレスにアクセスしてそのまま利用することが可能です。通常の利用はこちらで十分です。WindowsやMac環境からも全く同じように利用できます。

Scratch公式サイト

プログラミングといっても、テキストエディタを使ってコードを書くものとは異なり、さまざまな機能を持ったブロックをマウスで操作してつなげてプログラムを作成します。また、絵を描くためのツールがあったり、音声(音楽)ファイルを追加して効果音を付けたり、プログラミングに必要な素材もさまざまなものが提供されています。簡単なゲームなどを作ることが可能です。公式サイトには世界中のユーザが作った作品が多数公開されていますので、ぜひ一度、覗いてみてください。

scratch

ローカル環境にインストールすることも可能です。こちらからインストールするとRaspberry Pi用の拡張機能を利用してGPIOにアクセスすることができます。

sudo apt install scratch3

※VNC経由で使っていたりモバイル用途の小さなディスプレイでは解像度が足りず、一部のボタンが隠れてしまうことがあります。利用する場合は、ある程度大きなディスプレイ環境を用意してください。

Sonic PiでBGMを製作

「Sonic Pi」というアプリを使って、Ruby言語でさまざまな効果音を合成したり音楽を作ったりすることができます。公式サイト(sonic-pi.net)からダウンロードできます。Raspberry Pi OS向けには64bit版(Trixie以降)用の .deb パッケージが配布されており、以下のコマンドでインストールできます。

curl -o /tmp/sonic-pi.deb https://sonic-pi.net/files/releases/v4.6.0/sonic-pi_4.6.0_2_trixie.arm64.deb
sudo apt update
sudo apt install /tmp/sonic-pi.deb

インストール中に「jackd2の設定」として「リアルタイムプロセス優先度を有効にしますか?(Enable realtime process priority?)」と尋ねられることがあります。これは音声処理エンジンの遅延を最小化するための設定です。Raspberry Piは個人で利用する1台のコンピュータなので、「はい(Yes)」を選んで問題ありません。

音声出力について(Raspberry Pi 5)

Raspberry Pi 5ではアナログイヤホンジャックが廃止されています。また、現在のRaspberry Pi OS(Trixie)はオーディオに PipeWire を採用しているため、Sonic Piの音声出力先はデフォルトでHDMIになります。Sonic Piのインストーラーに同梱されている qpwgraph を使うことで、HDMI以外のスピーカーやBluetoothスピーカーへ出力先を切り替えることができます。

sonicpi

以下はSonic Pi公式サイトに掲載されていたサンプルプログラムです。このコードを「Sonic Pi」上で実行すると、無限に繰り返す不思議なBGMが再生されます。

with_fx :reverb, mix: 0.2 do
  loop do
    play scale(:Eb2, :major_pentatonic, num_octaves: 3).choose,
      release: 0.1, amp: rand
    sleep 0.1
  end
end

以下に示すサンプルコードも公式のチュートリアルをベースにしています。詳しく知りたい場合はアプリに付属のチュートリアル(英語)を参照してください。

Sonic PiもScratchなどと同様にライブコーディング(その場で書いて、その場で動かす)ができます。あまり大きく構えずにギターを片手に持ったくらいの気持ちで気軽に音を出してみましょう。 以下のサンプルを入力してみましょう。

live_loop :flibble do
  sample :bd_haus, rate: 1
  sleep 0.5
end

「Run」ボタンを押してみてください。バスドラムの音が聴こえましたか?もし演奏を止めたい場合は「Stop」ボタンを押してください。もしくは、(音を出し続けたままでも)「sleep 0.5」と書かれた箇所を「sleep 1」と書き換えて、もう一度「Run」ボタンを押してみてください。ドラムのテンポが変わりましたか?

これがライブコーディングです。その場で簡単に書き換えて違いを試すことができます。さらにもう少し試してみましょう。先ほどのコードに「sample :ambi_choir, rate: 0.3」という行を足します(全体では以下のようになります)。

live_loop :flibble do
  sample :ambi_choir, rate: 0.3
  sample :bd_haus, rate: 1
  sleep 1
end

数字を色々と書き換えて変化を試してみてください。ただし、「sleep」の値を0に近づけるとどうなるでしょうか?段々とテンポが速くなるのですが、どこかの時点でSonic Piが演奏しきれなくなりエラーを出してストップしてしまいます。このような場合は、エラーの出ないところまで大きな値に戻してあげる必要があります。

以下のように行の先頭に「#」をつけると、その行が無視されて演奏されなくなります。

live_loop :flibble do
  sample :ambi_choir, rate: 0.3
#  sample :bd_haus, rate: 1
  sleep 1
end

もう一つ、以下のサンプルでは、二つのループが同時に動いています。 ・sleepを調節してテンポを変更してみてください ・コメントを外してみてください

live_loop :guit do
  with_fx :echo, mix: 0.3, phase: 0.25 do
    sample :guit_em9, rate: 0.5
  end
#  sample :guit_em9, rate: -0.5
  sleep 8
end

live_loop :boom do
  with_fx :reverb, room: 1 do
    sample :bd_boom, amp: 10, rate: 1
  end
  sleep 8
end

感触が掴めたでしょうか?思いついたら好きな部分を書き換えてみて、音がどんな風に変化するか確認してみてください。Sonic Piに付属のチュートリアルにはもう少し詳しい解説(英語)が載っています。サンプルもたくさん掲載されていますので、それらを参考にするのも良いでしょう。

こちらがこの節の最後のサンプルです。どんな音が聞こえそうか想像がつきますか?

play :C
sleep 0.5
play :D
sleep 0.5
play :E
sleep 0.5
play :F
sleep 0.5
play :G

ここまでのサンプルで使われていたのが、冒頭でも紹介したRubyという言語です。この構文を理解するには、シンボルやブロックの概念を理解する必要がありますが、逆にいうとそれ以上の特別なことはしていません。Rubyはとても柔軟性の高い言語で、従来の手続き型のプログラミング、オブジェクト指向の要素を持ちつつ、こうした特殊な用途にもすんなりと対応することができます(詳しく知りたい方は、「DSL:ドメイン固有言語」というキーワードで調べてみてください)。

スクリーンショットを撮る(grim)

現在のRaspberry Pi OS(Trixie)では、以下のコマンドが利用できます。または PrtScrキーを押すだけでもスクリーンショットを撮ることができます(DesktopではなくPicturesフォルダに保存されます)。

grim

3秒後にカーソル位置を含めたスクリーンショットを撮る場合は以下のようにします。

sleep 3 && grim -c 

MinecraftのようなゲームLuantiで遊ぶ

Luanti(旧称:Minetest)は、MinecraftのようなブロックベースのオープンワールドゲームをRaspberry Pi上でも動かすことができる、オープンソースのゲームエンジンです。Raspberry Pi向けのMinecraft無料版(minecraft-pi-reborn)も存在しますが、Pi 5ではカーネルのページサイズ変更(4KB→16KB)の影響で執筆時点では動作しないため、本書ではLuantiを紹介します。Luantiの面白いところは、ゲーム内のルールやアイテム・ブロックのすべてが Lua というスクリプト言語で定義されていることです。自分でmodを書けば、オリジナルのブロックや道具、ルールをゲームに追加することができます。

以下のコマンドでインストールできます。

sudo apt install luanti

インストールが完了したらメニューから「Luanti」を起動し、ゲームをインストールして遊んでみましょう。最初に表示される「コンテンツ」のブラウザから Minetest Game を選ぶと、Minecraftに近い遊び方ができます。

Luaとmod

Luanti のゲームやmodはLua言語で書かれています。Luaはシンプルな構文を持つ軽量なスクリプト言語で、ゲームの拡張によく使われています。modのファイルはただのテキストファイルなので、テキストエディタ一つで作れます。

modを置く場所は ~/.minetest/mods/ です。試しに最小限のmodを作ってみましょう。

mkdir -p ~/.minetest/mods/mymod

~/.minetest/mods/mymod/mod.conf というファイルを作成して、以下の内容を書きます。

name = mymod
description = My first Luanti mod

次に ~/.minetest/mods/mymod/init.lua を作成します。以下は、「光るガラス」という名前のオリジナルブロックを追加するサンプルです。

minetest.register_node("mymod:glowing_glass", {
    description = "光るガラス",
    drawtype = "glasslike",
    tiles = {"default_glass.png"},
    paramtype = "light",
    light_source = 14,
    groups = {cracky = 3},
})

modを有効化するには、ワールドの選択画面で「modを選択」→ mymod を有効にしてからゲームに入ります。クリエイティブモードで i キーを押してインベントリを開き、検索欄に glowing などと入力すると「光るガラス」が見つかります。そこからドラッグしてホットバーに持っていけば配置できます。

light_source の数値(最大15)を変えると明るさが変わります。description を変えれば名前も自由に変えられます。

チャットコマンドを追加する

もう少しプログラミングらしいことをやってみましょう。init.lua に以下を追記すると、チャットで /scatter と打つたびに足元の周囲へ光るガラスをランダムにばらまくコマンドが追加されます。

minetest.register_chatcommand("scatter", {
    description = "足元に光るガラスをばらまく",
    func = function(name)
        local player = minetest.get_player_by_name(name)
        local pos = player:get_pos()
        for i = 1, 10 do
            local x = pos.x + math.random(-5, 5)
            local z = pos.z + math.random(-5, 5)
            minetest.set_node({x=x, y=pos.y, z=z}, {name="mymod:glowing_glass"})
        end
        return true, "光るガラスをばらまきました!"
    end,
})

func = function(name) ... end の部分がチャットコマンドを受け取ったときに呼ばれる関数です。for i = 1, 10 do で10回繰り返し、math.random(-5, 5) で−5から+5のランダムな値を取得して、プレイヤの足元周辺の座標を計算しています。ゲーム内でTキーを押してチャット欄を開き、/scatter と入力してみてください。

10 の数を増やせばたくさんばらまけます。math.random の範囲を広げれば遠くまで飛ばせます。こうした小さな変更を試しながら、コードとゲームの世界のつながりを実感してみてください。

mod作りをもっと深く学びたい場合は、コミュニティが整備した Luanti Modding Book (英語)が参考になります。

日本語入力を後からインストール

インストール時に日本語を選択していれば、最初から Ctrl + Space で利用できる日本語環境が用意されています。後から入れたい場合は、以下のコマンドを実行します。

sudo apt install fcitx5-mozc fonts-takao

インストール後、再起動を行ってください。

画面右上にキーボードのアイコンが表示されているので、ここから設定画面を開き、「Mozc」を追加します。

Tips:

Mozc Settings → Keymap style → Customize… メニューから以下のように設定すると、mac風の「変換/無変換」の動作を再現できます。

DirectInput  Henkan  Activate IME
Precomposition  Muhenkan  Deactivate IME
Conversion  Muhenkan  Deactivate IME
Composition  Muhenkan  Deactivate IME

日本語を喋らせてみる

Raspberry Piにスピーカを接続して日本語を喋らせることもできます(HDMIで接続したモニタに音源が付いている場合でもOKです)。

必要なパッケージを追加します。

sudo apt install open-jtalk open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001

辞書と音声ファイルを明示する必要があります。wav形式で保存します。

echo "こんにちは" | open_jtalk -x /var/lib/mecab/dic/open-jtalk/naist-jdic -m /usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice -ow test.wav

テストには aplay というコマンドが利用できます。

aplay test.wav

上記でインストールしたのはOpenJTalkに標準で含まれる男性の声です。下記のサイトでは女性の声のサンプルを手にいれることができます。アーカイブを展開した中にある拡張子が .htsvoice のファイルを上記の open_jtalk コマンド実行時に指定してください。

MMDAgent_Example-1.7(SourceForge)

デジタルサイネージを作ってみる

現在のRaspberry Pi OS(Trixie)では、labwcという仕組みを使います(LXDE版は前著などを参照してください)。

デジタルサイネージというのは、駅や店頭に設置されたディスプレイに、訪れる人に有益な情報(運行情報、営業時間)や広告などを表示する端末のことです。Raspberry Piを普通に起動するとWindowsやMacのようなデスクトップ画面が表示されますが、この設定を変更してウェブブラウザだけを固定して表示することで実現できます。今回使用するChromiumブラウザでは「キオスク」モードと呼ばれています(デジタルサイネージと似ていますが、ユーザの操作を受け付けたり、もう少しインタラクティブ性を持ったものの呼称に使われることが多いようです)。

.config/labwc/autostart に直接コマンドを記述します。

chromium --kiosk --incognito https://example.com/path/to/page

システムを再起動すると指定したインターネット上のウェブサイトが全画面表示された状態になって起動します。マウスカーソルを出してユーザの操作も受け付ける場合は、以上で完了です。固定の情報を安定して表示させたい場合は、ローカルに置いたHTMLファイルを表示すると良いでしょう。以下は横向きに文字が流れるようなサイネージの例です。HTMLとCSSで簡単に作ることができます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RaspberryPi Signage 2026</title>
    <style>
        html, body {
            cursor: none !important;
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            background-color: #000;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
        }

        .ticker-container {
            width: 100%;
            overflow: hidden;
            background: rgba(255, 255, 255, 0.1);
            padding: 20px 0;
        }

        .ticker-text {
            display: inline-block;
            white-space: nowrap;
            font-size: 5rem;
            font-weight: bold;
            color: #fff;
            text-shadow: 0 0 15px rgba(255, 255, 255, 0.8);
            
            animation: ticker-animation 15s linear infinite;
        }

        @keyframes ticker-animation {
            0% {
                transform: translateX(100%);
            }
            100% {
                transform: translateX(-100%);
            }
        }
    </style>
</head>
<body>
    <div class="ticker-container">
        <div class="ticker-text">
            Dive into RaspberryPi 2026 &nbsp;&nbsp;&nbsp;&nbsp; 
            Dive into RaspberryPi 2026 &nbsp;&nbsp;&nbsp;&nbsp;
        </div>
    </div>
</body>
</html>

上記のファイルをホームディレクトリにsignage.htmlとして保存した場合、autostartに記述するコマンドは以下のようになります。--password-store=basic は、Chromiumがキオスクモードで起動した際に、「Unlock Keyring」のダイアログを表示しないようにするためのオプションです。また、wlrctlというツールを使って、10秒後にマウスカーソルを少しだけ動かすコマンドも追加しています(マウスが動かないとカーソルを非表示にするスタイルが効かないため)。wlrctlは以下のコマンドでインストールしておきます。

sudo apt install wlrctl
(sleep 10 && wlrctl pointer move +1) &
chromium --kiosk --incognito --password-store=basic signage.html

Ctrl+W で、Chromiumを終了させることができます。再度起動するには、OSを再起動するか、ターミナルから上記のコマンドを直接実行してください。

GIMP(GNU Image Manipulation Program)

Raspberry Piも5になって、いよいよミニデスクトップPCとしての利用価値も高まって来ているように見えます。定番のお絵描きツールであるGIMPとInkscapeを試してみましょう。まずはインストールします。

sudo apt install gimp 

インストールが済むとメニューに項目が追加されているはずですので、そこから起動します。

GIMPは、ビットマップ(ピクセル)画像を編集するための高機能な画像編集ソフトウェアです。写真の加工やレタッチ、デザインの作成など、Adobe Photoshopに匹敵する機能を持つフリーソフトウェアとして世界中で利用されています。

gimp

主な用途: - 写真の補正やレタッチ(明るさ、コントラスト、色調の調整) - スクリーンショットやWebグラフィックスの編集 - レイヤーやマスクを用いた複雑な画像合成 - フィルタエフェクトを使った画像加工 - テキストの追加や装飾

Raspberry Pi 5では、ちょっとした画像編集や文字入れなどの作業は、ほぼストレスなく行うことができます。ただし、非常に大きな画像ファイルを扱ったり、複雑なフィルタ処理を行う場合は、処理に時間がかかることがあります。

実践例:印影を透過PNGに変換する

GIMPの便利な使い方の例として、判子の画像から印影だけを取り出して透過PNGにする方法を紹介します。これができるようになると、PDFに捺印したような画像を作成したり、さまざまな場面で応用できます。判子の写真から印影部分だけを抽出して、背景が透明な印影画像に加工する手順を見ていきましょう。念のため書いておくと、この方法はあくまで画像編集のテクニックの一例であり、実際の捺印の法的効力やセキュリティについては別途考慮が必要です。

手順:

  1. 画像を開く
    元画像を右クリックして「Open With」→「GIMP」と進み、GIMPで開きます。コマンドラインからは gimp 画像ファイル名 でも起動できます。下の画像は、判子の写真をGIMPで開いた状態です。
GIMPで画像を開いた状態
  1. 色で選択ツールを選ぶ
    左上のツールボックスから「Select by Color Tool(色で選択ツール)」を選択します。ツールボックスの右上にあるスポイトのようなアイコンです。
Select by Color Toolを選択
  1. 印影を選択
    カーソルの形が変わるので、そのまま印影の赤い部分の上をクリックします。印影の周りに点線(行進する蟻)が現れたら、うまく選択されています。もし異なる範囲が選択されてしまう場合は、左下のツールオプションで「Threshold(しきい値)」を調整しながら何度か試してみてください。
印影が選択された状態
  1. 選択範囲をコピー&ペースト
    この状態で、選択範囲を「Edit」→「Copy」してから「Edit」→「Paste」します(Ctrl+CとCtrl+Vでも可)。
Copy & Paste後
  1. レイヤーを作成
    右下のレイヤー一覧に「Floating Selection(フローティング選択範囲)」というレイヤーが登場しているはずです。この状態で、レイヤーパネルの下部にある「New Layer(新しいレイヤーを作成)」アイコン(+マークのついたアイコン)をクリックします。すると、「Floating Selection」が「Pasted Layer」に変わります。
Floating Selectionをレイヤー化
  1. 元のレイヤーを削除
    その下の元画像レイヤーは不要なので削除します。元画像レイヤーをクリックしてから、レイヤーパネル下部の「Delete Layer(レイヤーを削除)」アイコン(×マーク)をクリックして削除します。
元レイヤーを削除
  1. 左右反転とトリミング
    印影が逆さまになっている場合、左右反転します。「Image」→「Transform」→「Flip Horizontally」を実行してください。
左右反転後
  1. 余白をトリミングしてエクスポート
    余白ができてしまった場合は、「Image」→「Fit Canvas to Layers」を実行してトリミングします。これで余白が自動的に削除されます。
印影を透過PNGに変換する

最後に、できあがったファイルを書き出します。「File」→「Export As…」を選択し、ファイル名を指定します。拡張子を.pngにすることで、PNG形式で保存されます。保存時のダイアログで「Save background color」のチェックを外し、透過情報が保持されるよう設定してください。

これで透過PNGの印影画像ができあがりです。この画像をPDFやWord文書の上に配置すれば、捺印したような見た目を作ることができます。ここまでの手順は、慣れれば数分で終わります。裏を返せば、紙やPDFに押された印影はこれだけ簡単に複製できてしまう、ということです。押印を本人確認の手段として扱う慣習はいまだに残っていますが、その限界も実感してもらえたのではないでしょうか(もちろん、他人の印影を無断で複製して使うことは犯罪です)。

このように、GIMPには色で選択する機能や透過画像を作成する機能など、実用的な機能が多数備わっています。慣れてくると、写真の背景を削除したり、複数の写真を合成したりといった高度な編集も可能になります。

Inkscape

Inkscapeは、ベクター(パス)画像を作成・編集するためのグラフィックソフトウェアです。SVG(Scalable Vector Graphics)形式を標準フォーマットとして使用しており、拡大縮小しても画質が劣化しないという特徴があります。Adobe Illustratorの代替ソフトウェアとして広く知られています。インストールは以下のコマンドで行います。

sudo apt install inkscape
inkscape

主な用途: - ロゴやアイコンのデザイン - ポスターやチラシなどの印刷物のデザイン - イラストや図形の作成 - 技術図面や回路図の作成 - Webサイト用のグラフィックス作成

ちょっとした文字や図形を描く程度であれば、ほとんど問題なく滑らかに動きます。本書で使われている図の多くも、Inkscapeで作成されています。

GIMPとInkscapeの使い分け

詳しい使い方に興味のある方は、書籍やウェブを探してみてください。AdobeのIllustratorやPhotoshopほどではありませんが、日本語の情報も含めてそこそこの資料が見つかると思います。また、公式サイトにはチュートリアルやドキュメントも豊富に用意されています。

ローカルLLMを試す(Ollama)

ここまでの課題では、カメラ・Webサーバ・音声・ゲームなどを扱ってきました。最後に、Raspberry Pi 5でも実用できる「生成AI(LLM)」を試してみましょう。

LLM(Large Language Model)は、大量のテキストを学習した言語モデルです。文章の要約、アイデア出し、プログラム例の作成、文章の校正などに活用できます。多くのAIサービスはクラウド上で動作しますが、Ollama を使うと、手元のマシン(ローカル)でモデルを動かすことができます。

ローカル実行のメリットは、以下のような点です。

一方で、Raspberry PiではPCやクラウドGPUに比べて推論速度が遅くなる場合があります。用途に応じて、軽量モデルの選択や、クラウドとの使い分けを検討してください。

まずはインストールします。

curl -fsSL https://ollama.com/install.sh | sh

続けてモデルを取得します。ここでは gemma3 を使います。

ollama pull gemma3
ollama ls

取得できたら、対話を開始します。

ollama run gemma3

以下は実行例です(>>> の後ろがユーザが入力した内容です)。

$ ollama run gemma3
>>> Hello. Who are you?
Hello there! I'm Gemma, a large language model created by the Gemma team 
at Google DeepMind. I'm an open-weights model, which means I’m widely 
available for public use! 

I can take text and images as inputs and generate text-based responses. 

It’s nice to meet you!

>>> Could you say it in Japanese?
はい、こんにちは!私はジェンマで、Google DeepMind のチームによって作られた大
規模言語モデルです。私はオープンウェイトモデルで、一般公開されています。

(Hai, konnichiwa! Watashi wa Gemma de, Google DeepMind no shīmaisu no 
gakusai gengo moderu desu. Watashi wa open wēito moderu de, ippan kōkai 
sarete imasu.)

Roughly translated: "Hello! I am Gemma, a large language model created by 
the Google DeepMind team. I am an open-weight model and am publicly 
available." 

Would you like me to elaborate further, or perhaps answer a question in 
Japanese?

終了する場合は Ctrl + D を押します。

Pythonから利用する

対話画面だけでなく、Pythonプログラムからも呼び出せます。ライブラリはvenvという仮想環境にインストールします(venvについては実践課題6で詳しく説明します)。

python3 -m venv ~/venv
~/venv/bin/pip3 install ollama

以下のサンプルを実行する際も、~/venv/bin/python3 ファイル名 のようにvenv内のpythonを指定してください。

以下は、画像を入力として説明文を生成するサンプルです(ollama-pythonのREADMEの例をもとにしています)。image.png は任意の画像ファイルに置き換えてください。

import ollama

response = ollama.chat(
  model="gemma3",
  messages=[
    {
      "role": "user",
      "content": "Describe the image",
      "images": ["./image.png"],
    }
  ],
)

print(response["message"]["role"])
print(response["message"]["content"])

ヘビのイラスト(いらすとや)を入力した際の出力例は以下の通りです。

ヘビのイラスト
assistant
Here's a description of the image:

The image is a cute, cartoon-style illustration of a blue snake. 

Here's a breakdown of its features:

*   **Color:** The snake is a soft, pale blue color with irregular, white polka dots.
*   **Shape:** It's a coiled snake, curving upwards in a relaxed posture.
*   **Facial Features:** It has small, dark black eyes and a small, black bow tie.
*   **Hat:** The snake is wearing a black top hat. 

Overall, it's a whimsical and adorable depiction of a snake dressed as a dapper gentleman.

ただし、Raspberry Piでの画像入力は処理時間が長く、著者の試行でも実用には厳しいと感じました。用途によっては、画像だけクラウドAPIを使う、あるいはより軽量なモデルに切り替える、といった使い分けを推奨します。

参考URL

Raspberry PiでLLMを使えるようになると、これまでの課題(カメラ、音声、Web)と組み合わせた応用が一気に広がります。例えば、カメラ画像を説明してWebに掲示する、センサ値を要約して音声で読み上げる、といった仕組みも作れるようになります。

実践課題3 GPIOで電子工作

Raspberry Piの魅力の一つはGPIOを初めから備えていて、各種電子部品を簡単に接続することができる点です。LEDを光らせたり音を出したり、あるいはモータを駆動させて動き回ったり、逆に、光や音を検知してなんらかのアクションに結びつけたり、コンピュータが現実の世界とのやり取りをできるようになると活用の幅がぐっと広がります。

GPIOとは、「General Purpose Input Output」の頭文字をとったもので、日本語にすると汎用の入出力、といった意味になります。Raspberry Pi以外にも多くのマイコンボード(例えばArduinoなど)でも使われている仕組みです。本体の端にたくさんのピンが立っている部分がRaspberry PiのGPIOです。次節からRubyとrgpioを使い、GPIOの基本的な使い方を確認してゆきます。以下の部品があれば、実際に動かしてみることも可能です。

本章では、LinuxのGPIO character device APIをRubyから利用するrgpioライブラリを使います。Raspberry Pi OS Trixie以降、Ruby 3.3以降、libgpiod 2.xが必要です。ライブラリをインストールしていない場合は、次のように準備します。

sudo apt install libgpiod3
gem install rgpio

ピンの配置は以下のとおりです(本書用に作成した図です)。

ピン配置図

さらに細かい役割については、公式サイトの説明を参照してください。本書では、電源(3.3V・5V)とGND(グランド、接地)の位置、GPIOの番号(青いラベルの「GPIO◯◯」の数字)を主に使います。丸の中の数字はピンの物理的な位置を表す番号で、GPIOの番号とは異なる点に注意してください(例えばGPIO4は7番ピンです)。GPIO12、13、18、19はPWM用、というのも覚えておいても良いかもしれません。

以前のRaspberry Pi向け書籍ではRPi.GPIOやgpiozero、I2Cにはsmbus(いずれもPython)を使っていましたが、本章の作例はすべて、Raspberry Pi 5でも利用できるRubyのrgpioに書き換えています。

GPIOへの出力(LED点滅)

まずは、LEDを点滅させる簡単な回路を組んでみましょう。以下のような回路を組み立ててください。黄色の線はGPIOの4番に、黒の線はGNDにつながっています。抵抗はどちら向きでも構いませんが、LEDは図に書いた向きでないと通電しませんので注意してください。この回路図は「Fritzing」というツールを使って描いたものです。

GPIOへの出力

この回路を動かすRubyプログラムは以下のようになります。nanoエディタなどを使ってgpio-out.rbとして保存してください。

require "rgpio"

led = Rgpio::LED.new(4)
begin
    5.times do
        led.on
        sleep 1
        led.off
        sleep 1
    end
ensure
    led.close
end

以下のコマンドで実行します。

ruby gpio-out.rb

1秒間隔で5回、LEDが点灯したでしょうか?

GPIOへの入力(スイッチ押下の検出)

次は、逆にGPIOのピンに対して入力があった(=電圧がかかった)状態の検出をしてみます。以下のような回路を組んでください。抵抗は先ほどのものと同じで、LEDの代わりにスイッチが配置され、GNDへの線(黒色)が無くなり、代わりに赤いジャンパ線が3.3Vの電源ピンに接続されています。ここでは説明のためジャンパ線の色分けをしていますが、もちろん、どの色を使って接続しても構いません。

プログラムは次のようになります。ファイル名はgpio-in.rbです。rgpioはカーネルのイベント通知を使って押下と解放を検出します。

require "rgpio"

button = Rgpio::Button.new(4)
begin
    button.when_pressed  { puts "Pressed" }
    button.when_released { puts "Released" }

    Rgpio.pause
ensure
    button.close
end

Rgpio::ButtonはGPIO4の状態変化を監視し、押下と解放のたびに対応するブロックを呼び出します。既定では内部プルダウンを使うため、この回路のようにスイッチを3.3V側へ接続します。Rgpio.pauseで監視を続け、終了するときは「Ctrl+C」を入力してください。

GPIOへの入力

以下は、ボタンが押された際にSlackへメッセージを送信する例です。Webhook URLを自分のものに置き換えてください。

require "rgpio"
require "json"
require "net/http"
require "uri"

def slack
    webhook_url = URI("https://.../services/...")
    payload = {  "Notification from Ruby.",  "Ruby-Bot",  ":smile_cat:" }
    Net::HTTP.post(webhook_url, JSON.generate(payload), "Content-Type" => "application/json")
end

button = Rgpio::Button.new(4)
begin
    button.when_pressed { slack }
    Rgpio.pause
ensure
    button.close
end

ボタンの押下時に呼び出すブロックとしてslackを登録しています。

サーボモータを制御

サーボモータは、普通の(くるくると回転する)モータとは異なり、角度を指定して軸を回転させることができるモータです。ラジコンのハンドルや舵を制御するのに使われている、と言うとピンと来る方もいらっしゃるかもしれません。ロボットの関節なんかの駆動にも使われたりします。このモータを制御するにはPWM信号というパルスを生成してあげる必要があります。

必要なもの

今回使用したサーボモータは、SG90 MicroServo という機種です(写真参照)。駆動用の電源は単三電池を4つつないだ電池パックを使用しています。VccとGNDを電池に接続し、PWMをGPIOの4番ピンに接続します。GNDはさらに、GPIO側のGND端子とも接続が必要です。Raspberry Pi本体の5V端子から電源を取ると、電力不足で本体が落ちてしまうという情報があったので、このような配線にしていますが、短時間の駆動であれば(直接電源を取っても)問題なさそうです。電池パックがない場合、5V端子から電源を取るように配線してください。

サーボモータ接続例
PWM = 橙(ほとんど黄)
Vcc = 赤(ほとんど橙)
GND = 茶
回路図

図では電池ボックスを単三電池2本で描いていますが、実際には単三電池4本を直列にした電池ボックス(6V)を使います。

これで準備完了です。Rgpio::ServoはソフトウェアPWMで50Hzの信号を生成します。GPIO4でも追加の設定なしで利用でき、closeすると信号を停止します。

require "rgpio"

servo = Rgpio::Servo.new(4)
begin
    loop do
        servo.min
        sleep 2
        servo.mid
        sleep 2
        servo.max
        sleep 2
    end
rescue Interrupt
    puts "\nStopped."
ensure
    servo.close
end

min、mid、maxで端・中央・反対側の位置を指定できます。サーボごとに動作するパルス幅が異なるため、端で唸ったり発熱したりする場合は使用を止め、仕様に合わせて範囲を調整してください。

参考記事:
- Raspberry Piでサーボモータ(SG90)を制御する - ラズパイでラジコン制作(pigpio利用例)

フルカラーLEDを制御してみる

4本足のLEDを光らせてみましょう。一番長い足がGNDで、その他の3本がそれぞれ、R(赤),B(青),G(緑)の3原色に対応しています(R,GND,B,Gの順)。赤、緑、青の各端子をGPIO2、GPIO3、GPIO4に接続してください。各色にはそれぞれ抵抗を入れます。

require "rgpio"

led = Rgpio::RGBLED.new( 2,  3,  4)
begin
    led.color = 
    sleep 1
    led.red = 0.5
    sleep 1

    [, , , , , ].each do |color|
        led.color = color
        sleep 1
    end

    100.times do |n|
        led.blue = n / 100.0
        sleep 0.1
    end
ensure
    led.off
    led.close
end

ADコンバータの利用(可変抵抗)

Raspberry Piではアナログ値を直接読み取る機能がないため、ADコンバータMCP3208を利用します。rgpioにはSPI経由で値を読み取るRgpio::MCP3208クラスがあります。MCP3208のVDDとVREFは3.3V、AGNDとDGNDはGND、CLK・DOUT・DIN・CSはそれぞれSPIのSCLK・MISO・MOSI・CE0に接続します。あらかじめSPIを有効にして再起動してください。

sudo raspi-config nonint do_spi 0
sudo reboot
MCP3208

コード(mcp3208.rb)は以下のようになります。

require "rgpio"

adc = Rgpio::MCP3208.new
begin
    loop do
        puts adc.value(0)
        sleep 0.5
    end
rescue Interrupt
    puts "\nStopped."
ensure
    adc.close
end

前節のフルカラーLEDの回路と組み合わせて可変抵抗を回すと色が変わるような仕掛けを作ってみましょう。

require "rgpio"

adc = Rgpio::MCP3208.new
led = Rgpio::RGBLED.new( 2,  3,  4)
begin
    loop do
        led.color = [adc.value(0), adc.value(1), adc.value(2)]
        sleep 0.02
    end
rescue Interrupt
    puts "\nStopped."
ensure
    led.close
    adc.close
end

MCP3208のCH0、CH1、CH2をそれぞれ赤、緑、青の明るさに割り当てています。

可変抵抗の代わりに各種センサを取り付けることができます。

light-sensor

上記のコードを少し改造して、例えば”部屋が明るくなった/暗くなった”時のように、状態が変わった時だけメッセージを表示するコードの例です。外部のシステムと連携する場合にすべての測定値を送信するのは現実的ではないケースなどで役に立つと思います。(実際はもう少しノイズの対応など必要になるかもしれません)

require "rgpio"

adc = Rgpio::MCP3208.new
threshold = 0.5
previous_state = false

begin
    loop do
        value = adc.value(0)
        new_state = value > threshold
        if new_state != previous_state
            before = previous_state ? "HIGH" : "LOW"
            after = new_state ? "HIGH" : "LOW"
            puts format("%s -> %s (%.2f)", before, after, value)
        end
        previous_state = new_state
        sleep 0.5
    end
rescue Interrupt
    puts "\nStopped."
ensure
    adc.close
end

モータドライバの利用

ここではDRV8835というモータドライバを使います。Rgpio::Motorは前進用と後退用の2本のGPIOを切り替えます。速度制御には対応していないため、モータは全速で動作します。

筆者の環境ではGPIO4が他の用途に使用中で使えなかったため、代わりにGPIO2を前進用に使っています。

require "rgpio"

motor = Rgpio::Motor.new( 2,  14)
begin
    loop do
        motor.forward
        sleep 5
        motor.backward
        sleep 5
    end
rescue Interrupt
    puts "\nStopping."
ensure
    motor.close
end

以下の図を参考に、VM,GND=モータの電源(+と-)、AOUT1,2=モータA、BOUT1,2=モータB、VCC=Raspberry Piの3.3V電源、AIN1,2=モータA制御用のGPIO(本例では2と14番)、BIN1,2=モータB制御用のGPIO(本例では未接続)と接続してください。

DRV8835

このドライバは2つのモータを同時に制御することが可能です。

温度センサの利用

ADT7410という温度センサからの値を取得するプログラムです(gpio-adt7410.rb)。保存して実行すると1秒ごとに測定した温度を表示します。

require "rgpio"

sensor = Rgpio::ADT7410.new
sleep Rgpio::ADT7410::CONVERSION_TIME  # 電源投入直後の最初の測定が終わるのを待つ
begin
    loop do
        puts format("%.2f C", sensor.temperature)
        sleep 1
    end
rescue Interrupt
    puts "\nStopped."
ensure
    sensor.close
end

以下のコマンドで実行します。

ruby gpio-adt7410.rb

このセンサとの接続にはI2Cという方式を使います。Rgpio::ADT7410は、I2C経由でセンサのレジスタを読み出し、摂氏の温度に変換して返してくれます(センサのアドレスは既定の0x48です)。先に出てきたSPI通信に比べると、I2Cのセンサは配線が幾分シンプルで慣れやすいメリットはあるかもしれません。VCCとGNDの他に2本の信号線をそれぞれ、SDA=GPIO2,SCL=GPIO3とつなげば完成です(次節のLCDディスプレイの説明中に写真があります)。

また注意点として、あらかじめ Control Centre → Interfaces において I2C を有効化しておかなければなりません。コマンドラインから有効化する場合は、SPIと同様に以下のようにして再起動します。

sudo raspi-config nonint do_i2c 0
sudo reboot

正しく有効化されているかどうかの確認には i2cdetect -y 1 というコマンドを使うと便利です(有効な場合、接続されているセンサのアドレス、上記の場合48が表示されます)。

センサから読み出した値(ビット列)を摂氏温度に変換する処理は、Rgpio::ADT7410の中で行っています。次章のPico版では、この変換を自分で書いていますので、見比べてみてください。仕組みを詳しく知りたい方は以下の書籍なども参照してみてください。

LCDディスプレイ

もう一つ、I2C接続のパーツを紹介します。LCD(小型の液晶ディスプレイ)です。ここでは秋月電子のAQM0802(ST7032コントローラ内蔵、8文字×2行)を使い、rgpioのRgpio::ST7032クラスで制御します。配線は温度センサと同じで、VCC・GNDと、SDA=GPIO2、SCL=GPIO3の2本の信号線を共有して接続します。I2Cでは、アドレスの異なる複数の機器を1組の信号線にまとめてつなぐことができます(LCDのアドレスは0x3eです)。

以下のプログラムをgpio-st7032.rbとして保存してください。実行すると、液晶ディスプレイに温度センサからの値が表示されます。

require "rgpio"

sensor = Rgpio::ADT7410.new
lcd = Rgpio::ST7032.new( 8)
sleep Rgpio::ADT7410::CONVERSION_TIME

begin
    lcd.move_to(0, 0)
    lcd.print("Temp".ljust(8))
    loop do
        lcd.move_to(0, 1)
        lcd.print(format("%.1f C", sensor.temperature).rjust(8))
        sleep 1
    end
rescue Interrupt
    puts "\nStopped."
ensure
    lcd.clear
    lcd.close
    sensor.close
end

move_to(列, 行)で書き込む位置を指定し、printで文字列を表示します。1行目の「Temp」は変わらないので最初に一度だけ書き、2行目の温度だけを毎秒上書きしています。毎回画面全体をクリアすると表示がちらつくため、rjust(8)で8文字分の幅に揃えて、前の表示を上書きで消しています。

文字が何も表示されない場合は、コントラストの調整が必要なことがあります。Rgpio::ST7032.new(columns: 8, contrast: 40)のように、0から63の範囲で値を変えながら試してください。

以下の写真は、温度センサとこのLCDを同時に接続した様子です。

adt7410 st7032

こちらを応用して、さまざまな値をディスプレイに表示してみましょう!可変抵抗,照度センサ,CPU温度etc… 例えばCPUのロードアベレージを表示する場合、ループ内で2行目に書き込む部分を以下のように書き換えます。

        load = File.read("/proc/loadavg").split.first
        lcd.print(load.rjust(8))

センサを組み合わせる場合、前節のADコンバータ(MCP3208)のコードを利用できます。浮動小数点をそのまま表示すると桁数が足りなくなってしまいますので、formatで小数点以下の桁数を指定して(丸めて)表示してください。

adc = Rgpio::MCP3208.new
:
        lcd.print(format("%.3f", adc.value(0)).rjust(8))

ロータリーエンコーダー

ロータリーエンコーダーは、つまみを回した向きと量を検出する部品です。オーディオ機器の音量つまみや、マウスのホイールなどに使われています。見た目は可変抵抗に似ていますが、可変抵抗のように端で止まることはなく、何回転でも回し続けることができます。回すと「カチッ」というクリック感があり、このクリック1つ分が1ステップになります。

可変抵抗はつまみの位置をアナログの電圧で出力するため、ADコンバータが必要でした。ロータリーエンコーダーはA相・B相という2本の信号をON/OFFで出力するので、GPIOに直接つなぐことができます。つまみを回すと2本の信号が少しずれてON/OFFを繰り返し、どちらが先に変化したかで回した向きが分かります。

必要なもの Raspberry Pi本体の他に以下のパーツが必要です。

KY-040には「CLK」「DT」「SW」「+」「GND」の5本の端子があります。CLKがA相、DTがB相、SWはつまみを押し込んだときに反応するスイッチです。以下のように結線してください。

「+」は必ず3.3Vにつないでください。モジュール上のプルアップ抵抗は「+」につながっているため、5VにつなぐとGPIOに5Vがかかり、Raspberry Piを壊してしまうおそれがあります。

プログラムは以下のようになります。gpio-rotary.rbとして保存してください。つまみを回すとステップ数を表示し、つまみを押し込むと0に戻します。

require "rgpio"

encoder = Rgpio::RotaryEncoder.new( 17,  18,  16)
button = Rgpio::Button.new(27,  true)
begin
    encoder.when_rotated_clockwise { puts "CW  #{encoder.steps}" }
    encoder.when_rotated_counter_clockwise { puts "CCW #{encoder.steps}" }
    button.when_pressed do
        encoder.steps = 0
        puts "Reset"
    end
    Rgpio.pause
ensure
    button.close
    encoder.close
end

以下のコマンドで実行します。

ruby gpio-rotary.rb

つまみを右(時計回り)に回すとCW、左(反時計回り)に回すとCCWと表示され、ステップ数が増減します。

CW  1
CW  2
CW  3
CCW 2
Reset

max_steps: 16は、ステップ数の範囲を-16から16に制限する指定です。範囲の端まで来るとそれ以上は増減しません。wrap: trueを加えると、端を越えたときに反対側の端から数え直します。時計回りなのに数が減っていく場合は、a:とb:の番号を入れ替えてください(CLKとDTの配線を入れ替えても同じです)。

KY-040のSWにはプルアップ抵抗が付いていないことが多いため、Rgpio::Buttonにはpull_up: trueを指定して内部プルアップを使っています。スイッチ押下の検出の節とは逆に、押すとGNDにつながる配線です。

終了するときは「Ctrl+C」です。

応用として、つまみでサーボモータの角度を変えてみましょう。サーボモータの節と同じように、サーボの信号線をGPIO4につないでおきます。encoder.valueはステップ数を-1.0から1.0の範囲に換算した値で、Rgpio::Servoのvalueと同じ範囲なので、そのまま渡すことができます。gpio-rotary-servo.rbとして保存してください。

require "rgpio"

encoder = Rgpio::RotaryEncoder.new( 17,  18,  16)
servo = Rgpio::Servo.new(4)
begin
    servo.mid
    encoder.when_rotated { servo.value = encoder.value }
    Rgpio.pause
ensure
    servo.close
    encoder.close
end

つまみを1クリック回すごとに、サーボが少しずつ回転します。max_stepsの値を変えると、1クリックあたりの角度を調整できます。

ドアの開閉で写真撮影(リードスイッチ)

リードスイッチは、磁石を近づけると内部の接点が閉じる(ONになる)スイッチです。ドアや窓の枠にスイッチ本体を、扉の側に磁石を取り付けておくと、扉の開閉を検知できます。防犯用の「開閉センサ」として、磁石とセットで販売されているものが使いやすいでしょう。ここでは、ドアが開いたらカメラで写真を撮る仕組みを作ってみます。

必要なもの Raspberry Pi本体の他に以下のパーツが必要です。

カメラ接続

配線は「GPIOへの入力(スイッチ押下の検出)」の節と同じで、スイッチの代わりにリードスイッチを接続します。リードスイッチには極性がないので、2本の線のどちらをどちらにつないでも構いません。

一般的な開閉センサは、磁石が近いとき(ドアが閉じているとき)にONになるタイプ(ノーマルオープン)です。逆の動作をするタイプ(ノーマルクローズ)の場合は、以下のプログラムのwhen_pressedとwhen_releasedを入れ替えてください。

まずは、この回路が正しく動くか検証するプログラムを書きましょう。nanoエディタで以下のコードを記述してgpio-door.rbとして保存してください。

require "rgpio"

door = Rgpio::Button.new(4)
begin
    door.when_pressed  { puts "closed" }  # 磁石が近づいた(ドアが閉じた)
    door.when_released { puts "opened" }  # 磁石が離れた(ドアが開いた)
    Rgpio.pause
ensure
    door.close
end

以下のコマンドで実行します。

ruby gpio-door.rb

リードスイッチに磁石を近づけたり離したりして、以下のように表示されることを確認してください。

closed
opened
closed

Rgpio::Buttonは「スイッチ押下の検出」で使ったものと同じクラスです。リードスイッチも、磁石で押されるスイッチだと考えれば同じように扱えます。機械的な接点には、ON/OFFが切り替わる瞬間に細かく何度も切り替わる「チャタリング」という現象がありますが、Rgpio::Buttonは既定でこれを取り除いてくれるため、1回の開閉で表示が1回だけ出ます。

確認が終わったら、「Ctrl+C」でプログラムを終了してください。

次は、ドアが開いたら写真を撮るプログラムです。先ほどのgpio-door.rbをもとに、ドアが開いたときのブロックから撮影コマンドを呼び出します。カメラの制御にはRaspberry Pi OSに含まれるrpicam-stillを使います。gpio-door-camera.rbとして保存してください。

require "rgpio"

def take_picture
    filename = Time.now.strftime("%Y-%m-%d_%H.%M.%S.jpg")
    puts filename
    system("rpicam-still", "--nopreview", "--timeout", "1000", "--output", filename)
end

door = Rgpio::Button.new(4)
begin
    door.when_released { take_picture }  # ドアが開いたら撮影
    Rgpio.pause
ensure
    door.close
end

rpicam-stillの--timeoutは撮影までの待ち時間をミリ秒で指定します。撮影前にカメラのプレビューを出す場合は--nopreviewを外してください。

Raspberry Pi公式のカメラモジュールがない場合は、USB接続のWebカメラでも撮影できます。fswebcamをインストールして、

sudo apt install fswebcam

take_pictureの中のsystemの行を次のように書き換えてください。

    system("fswebcam", "--no-banner", "-r", "1280x720", "-S", "10", filename)

-rは解像度、-Sは撮影前に読み捨てるフレーム数です。Webカメラは起動直後の数フレームが暗くなりやすいため、少し読み捨ててから保存しています。--no-bannerは画像下部に日時の帯を入れない指定です。

以上で、完成です。終了する場合は、gpio-door.rbと同様、「Ctrl+C」です。

撮影した写真をメールやSlackで通知したり(Slackへの通知は「スイッチ押下の検出」の節の例が使えます)、ドアが開いていた時間を記録したりすると、簡単な見守りや防犯の仕組みに発展させることができます。

測距センサを使って写真撮影

最後に、測距センサを使って前節と同じように写真を撮る例を紹介します。リードスイッチは扉の開閉しか分かりませんが、こちらはセンサの前を人や物が通ったことを検知できます。写真の右側に写っているのが今回使用しているセンサ(Sharp 2Y0A21)です。数十センチのオーダーにある物体までの距離を測るのが本来の用途ですが、今回はGPIOの4番ピンに測定端子をつないで、ON/OFFのみの検出に利用しています。なぜか、赤い線がGNDで、黒い線が5V端子です。

測距センサ

プログラムは以下のようになります。0.1秒ごとに4番ピンの状態を監視し、ONになった瞬間のみ、カメラを起動し、タイムスタンプを名前に付けて画像を保存しています。gpio-distance-camera.rbとして保存してください。

require "rgpio"

def take_picture
    filename = Time.now.strftime("%Y-%m-%d_%H.%M.%S.jpg")
    puts filename
    system("rpicam-still", "--nopreview", "--vflip", "--hflip", "--timeout", "1", "--output", filename)
end

sensor = Rgpio::InputDevice.new(4)
previous_state = false
begin
    loop do
        sleep 0.1
        current_state = sensor.value
        if current_state != previous_state
            puts "GPIO4 is #{current_state ? "HIGH" : "LOW"}"
            take_picture if current_state
        end
        previous_state = current_state
    end
rescue Interrupt
    puts "\nStopped."
ensure
    sensor.close
end

Rgpio::InputDeviceは既定で内部プルダウンを使います。valueは端子がHIGHのときにtrueを返します。

rpicam-stillの--timeoutの値を調整することで、センサ感知後どれだけの間をおいて撮影するかを調整することができます。--vflip、--hflip(上下・左右の反転)は設置位置と向きに応じて適宜調整してください。

USB接続のWebカメラを使う場合は、前節と同様にsystemの行をfswebcamに書き換えます。上下左右を反転する場合は--rotate 180を加えてください。

    system("fswebcam", "--no-banner", "-r", "1280x720", "-S", "10", "--rotate", "180", filename)

実践課題4 Pico Wでマイコン入門

前章まで扱ってきたRaspberry PiはLinuxが動くシングルボードコンピュータ(SBC)です。一方この章で扱う Raspberry Pi Pico(以下「Pico」)は、全く別のカテゴリの製品です。Picoはマイクロコントローラ(マイコン)ボードであり、OSは持たず、電源を入れると書き込まれたプログラムがそのまま直接動き始めます。

Raspberry Pi Raspberry Pi Pico
カテゴリ シングルボードコンピュータ マイコンボード
OS Linux なし
起動 OSのブートに数十秒 電源投入後即時起動
消費電力 数W〜 数十mW〜
価格 数千円〜 数百円〜
用途 Webサーバ、デスクトップ、AI処理など センサ制御、IoT端末、組み込み機器など

OSやファイルシステムを持たない分、起動が瞬時で消費電力も非常に小さいのがPicoの特徴です。電池で長時間動かしたい用途や、決まった処理だけをシンプルかつ確実にこなしたい場面に向いています。逆に、Webブラウジングや複雑な処理、大量のデータ保存にはRaspberry Piの方が適しています。「大きなことはPiに、シンプルな入出力はPicoに」という役割分担と考えると分かりやすいでしょう。

PicoとPico Wの違い

Picoには無線通信機能のない無印「Pico」と、Wi-FiおよびBluetooth機能を内蔵した 「Pico W」 があります。両者はピン配置や基本的な使い方は同じですが、Pico WはWi-Fi経由でデータを送ったりWebサーバとして動かすといった応用が広がります。本章以降はPico Wを前提に話を進めます。価格差もわずかですので、これから購入する場合はPico Wを選んでおくと良いでしょう。

プログラミングにはRubyを使います。マイコン上でRubyを動かすために、prremoteというコマンドラインツールと、軽量Ruby処理系のmruby/cを組み合わせます。Raspberry Pi上でRubyの.rbファイルを書き、prremoteコマンドでコンパイルしてPicoに転送する、という流れです。Pythonを使わずに、Rubyの文法でGPIOやI2Cを制御できるのが特徴です。

prremoteのセットアップ

prremoteは次の3つの要素で成り立っています。

要素 役割
prremote(gem) .rbファイルをコンパイルしてPicoに転送するCLIツール
mruby/cランタイム Pico上で動く軽量Ruby処理系(ファームウェア)
mrbc RubyコードをmrubyバイトコードにコンパイルするCLI(mruby 4.x付属)

Ruby 3.4以上のインストール

Raspberry Pi OS(Trixie)のaptリポジトリに含まれるRubyは3.3系です。prremoteはRuby 3.4以上を要求するため、rbenvを使って新しいバージョンをインストールします。

sudo apt install -y rbenv ruby-dev libyaml-dev
mkdir -p "$(rbenv root)"/plugins
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
rbenv install 4.0.7
rbenv global 4.0.7
echo 'eval "$(rbenv init -)"' >> ~/.bashrc

aptで入るruby-buildは定義が古く、最新のRubyをインストールできないため、ruby-buildはGitHubから最新版を取得しています。インストールにはしばらく時間がかかります(RPiのCPUでは数分)。完了後、新しいターミナルを開いてバージョンを確認します。

ruby --version
# ruby 4.0.7 ...

mrbc(mruby 4.x)のインストール

Rubyのソースをmrubyバイトコードに変換するコンパイラmrbcが必要です。sudo apt install mrubyで得られるのはmruby 3.x系でありprremoteと非互換なため、rbenvを使ってmruby 4.xをインストールします。

rbenv install mruby-4.0.0

インストールされたmrbcは~/.rbenv/versions/mruby-4.0.0/bin/mrbcに置かれます。prremoteは~/.rbenv/versions/mruby-*/bin/mrbcを自動で探してくれますが、これ以外のパスに置かれたmrbcを参照したい場合は、環境変数MRBCにパスを明示することもできます。

prremote gemのインストール

gem install prremote

Pico Wへのランタイム書き込み

prremoteがPico上で動くには、mruby/cをベースにした専用のランタイムファームウェアを書き込む必要があります。これはMicroPythonのファームウェアと同様に、BOOTSELモードを使って行います。

prremote install

コマンドを実行すると、以下の操作を促すメッセージが表示されます。

  1. PicoボードのBOOTSELボタン(基板上の白いボタン)を押し続けます
  2. USBケーブルでRaspberry Piに接続します
  3. BOOTSELを離します(RPI-RP2がUSBストレージとして表示される)

prremoteが自動でファームウェアをダウンロード・コピーし、Picoが再起動します。以後はBOOTSELを押さなくても、USBを挿すだけで認識されます。

接続の確認

prremote list

接続中のデバイスとポートが一覧表示されます。

prremote version
# prremote: 0.4.2
# runtime:  0.4.2 (/dev/ttyACM0)
# mrbc: mruby 4.0.0 (2026-04-20) (/home/piuser/.rbenv/versions/mruby-4.0.0/bin/mrbc)

gemのバージョン、ランタイムのバージョン、mrbcのバージョンが表示されれば正常です。

最後に、1行コードを直接Pico上で実行してみましょう。

prremote eval "puts 'hello'"
# hello

hello と返ってきたら準備完了です。

LED点滅(Hello World)

最初のプログラムとして、Pico W本体に内蔵されているLEDを点滅させてみましょう。外付けの部品は不要です。

Pico WではWi-Fiチップ(CYW43)に内蔵LEDがつながっているため、CYW43クラスを使って操作します。以下の内容をblink.rbとして保存してください。

CYW43.init
led = CYW43::GPIO.new(CYW43::GPIO::LED_PIN)

5.times do
  led.write(1)
  sleep 0.5
  led.write(0)
  sleep 0.5
end

保存したら、ターミナルから実行します。

prremote run blink.rb

Pico W本体の緑色のLEDが1秒間隔で5回点滅すれば成功です。

CYW43.init でWi-Fiチップを初期化し、CYW43::GPIO::LED_PIN が内蔵LEDに対応するピンを指します。led.write(1) で点灯、led.write(0) で消灯、sleep 0.5 で0.5秒待機します。

ずっと点滅させ続けるには

5.times を loop do に変えると停止するまで永遠に繰り返します。

CYW43.init
led = CYW43::GPIO.new(CYW43::GPIO::LED_PIN)

loop do
  led.write(1)
  sleep 0.5
  led.write(0)
  sleep 0.5
end

停止するにはターミナルで Ctrl+C を押してください。

開発中のワークフロー:watchコマンド

プログラムを修正するたびに prremote run を打つ代わりに、watch コマンドを使うとファイルの保存を検知して自動的に再実行します。

prremote watch blink.rb

エディタでblink.rbを編集・保存するたびに、Picoが即座に再実行されます。開発中はこの使い方が便利です。

外付けLEDとボタン

内蔵LEDの点滅が確認できたら、外付けのLEDとボタンをGPIOピンにつないでみましょう。前章(実践課題3)と同じ部品が使えます。

以降の回路図では具体的なピン番号(GP15やGNDなど)が登場します。下のピン配置図(ピンアウト)を手元に置いておくと配線の際に役立ちます。

Pico W ピン配置図

Pico Wは全部で40本のピンを持ちます。主に使うのは以下の3種類です。

種別 説明
GPn(汎用GPIO) デジタル入出力に使う。一部のピンはADC(アナログ入力)やI2C・SPI・UARTなどにも兼用できる
3V3 3.3Vの電源出力。センサやLEDなどへの電源供給に使う
GND グランド(0V)。回路の基準電位

ピン番号(基板上の数字)とGP番号(GPIOの論理番号)は異なる点に注意してください。例えばGP15は基板上の20番ピンに位置しています。回路図を組む際は必ずピンアウト図で対応を確認しましょう。

外付けLEDの点滅

以下の回路を組んでください。LEDのアノード(長い足)を330Ω(1kΩでも可)抵抗を介してGP15(ピン番号20)に、カソード(短い足)をGND(ピン番号18)に接続します。

Pico W LED回路
led = GPIO.new(15, GPIO::OUT)

loop do
  led.write(1)
  sleep 0.5
  led.write(0)
  sleep 0.5
end

prremoteのGPIOクラスがMicroPythonのmachine.Pinに相当します。前章(実践課題3)のRaspberry Pi版と比べてみましょう。

Raspberry Pi(Ruby/rgpio) Pico W(prremote/Ruby)
ライブラリ require "rgpio" GPIO(ビルトイン)
ピン取得(出力) Rgpio::LED.new(4) GPIO.new(15, GPIO::OUT)
点灯 led.on led.write(1)
消灯 led.off led.write(0)

ボタンの押下検出

次にボタンを追加します。タクトスイッチの片側をGP14(ピン番号19)に、もう片側をGND(ピン番号18)に接続してください。

Pico W LED SWITCH回路
led = GPIO.new(15, GPIO::OUT)
btn = GPIO.new(14, GPIO::IN_PULLUP)

loop do
  if btn.read == 0   # ボタンが押されている(GNDに落ちている)
    led.write(1)
  else
    led.write(0)
  end
  sleep 0.05
end

GPIO::IN_PULLUP は内部プルアップ抵抗を有効にする定数です。ボタンを押すとGNDにつながってbtn.readが0を返し、離すと1に戻ります。

前章(実践課題3)のPi版ではRgpio::Buttonのコールバックでプレスとリリースを検出しましたが、Picoではポーリング(一定間隔で状態を確認するループ)で実装するのが基本です。よりシンプルな処理で確実に動くのがマイコンらしい書き方です。

別解:PULLDOWNを使う

PULLUPの反対が GPIO::IN_PULLDOWN(内部プルダウン)です。ボタンの片側をGNDではなく 3.3V につなぐと、押したときにbtn.readが1を返すようになります。これによりbtn.readの値をそのままled.writeに渡せます。

led = GPIO.new(15, GPIO::OUT)
btn = GPIO.new(14, GPIO::IN_PULLDOWN)

loop do
  led.write(btn.read)
  sleep 0.05
end
PULLUP PULLDOWN
ボタンの接続先 GND 3.3V
押していないとき btn.read → 1 btn.read → 0
押しているとき btn.read → 0 btn.read → 1
LEDの書き方 if btn.read == 0 で反転 led.write(btn.read) そのまま

回路の配線を変える必要がありますが、コードはよりシンプルになります。

ADコンバータの利用(可変抵抗)

前章では、Raspberry PiにはアナログInputが備わっていないため外付けのADコンバータ(MCP3208)をSPI経由で使いました。PicoにはADC(アナログ-デジタルコンバータ)が内蔵されているため、外付けチップなしに可変抵抗や照度センサなどのアナログ電圧を直接読み取ることができます。

ADCに対応したピンはGP26(ADC0)、GP27(ADC1)、GP28(ADC2)の3本です。今回はGP26に可変抵抗(10kΩ)を接続します。

用意するもの

可変抵抗の3本の端子のうち、両端の2本をそれぞれ3.3V(ピン番号36)とGND(ピン番号38)に、中央の端子(ワイパー)をGP26(ピン番号31)に接続します。

adc = ADC.new(26)  # GP26 = ADC0

loop do
  value   = adc.read              # 0〜65520(16ビットにスケール済み)
  voltage = value * 3.3 / 65_535
  puts "#{value} (#{voltage} V)"
  sleep 0.5
end

ADC.new(26) でGP26(ADC0)を初期化します。引数はGPIOのピン番号(26〜28)です。read メソッドは内部的にはRP2040の12ビットADC(0〜4095)を読み取り、16ビット(0〜65520)にスケールして返します。可変抵抗を回すにつれ、値が変化するのを確認してください。

Raspberry Pi(Ruby/rgpio) Pico W(prremote/Ruby)
外付けADC MCP3208(SPIで接続) 不要(内蔵ADC)
読み取り adc.value(0)(0.0〜1.0) adc.read(0〜65520)

フルカラーLEDを制御

PWMクラスを使って各チャンネルを制御します。PWM.newに周波数(frequency:)と初期デューティ比(duty_u16:)を名前付き引数で渡し、後からduty_u16 =で変更します。

用意するもの

GP16(緑)、GP17(青)、GP18(赤)にそれぞれ330Ω抵抗を介してLEDを接続します。カソード(共通の最も長い足)はGND(ピン番号23)につなぎます。

red   = PWM.new(18,  1000,  0)
green = PWM.new(16,  1000,  0)
blue  = PWM.new(17,  1000,  0)

def set_color(r_pwm, g_pwm, b_pwm, r, g, b)
  r_pwm.duty_u16 = (r * 65535).to_i
  g_pwm.duty_u16 = (g * 65535).to_i
  b_pwm.duty_u16 = (b * 65535).to_i
end

colors = [
  [1, 0, 0],  # 赤
  [0, 1, 0],  # 緑
  [0, 0, 1],  # 青
  [1, 1, 0],  # 黄
  [0, 1, 1],  # シアン
  [1, 0, 1],  # マゼンタ
]

loop do
  colors.each do |r, g, b|
    set_color(red, green, blue, r, g, b)
    sleep 1
  end
end

duty_u16 = は代入風のメソッド呼び出しで、0〜65535の整数でデューティ比を指定します。(r * 65535).to_i で0.0〜1.0の浮動小数点を整数に変換しています。

可変抵抗と組み合わせる

前節のADC回路に加えてGP27・GP28にも可変抵抗を追加すると、各色を独立して調整できます。

red   = PWM.new(18,  1000,  0)
green = PWM.new(16,  1000,  0)
blue  = PWM.new(17,  1000,  0)

adc_r = ADC.new(26)
adc_g = ADC.new(27)
adc_b = ADC.new(28)

loop do
  red.duty_u16   = adc_r.read
  green.duty_u16 = adc_g.read
  blue.duty_u16  = adc_b.read
  sleep 0.05
end

前章のRgpio::RGBLEDはドライバが自動で各色をPWM制御してくれましたが、Picoでは自分でPWMを組み合わせることで同じことができます。仕組みが見えるので、PWMの動きを理解する良い練習にもなります。

サーボモータを制御

前章ではRaspberry PiのRgpio::Servoを使ってSG90サーボを制御しました。PicoでもPWMの仕組みは同じです。

用意するもの

SG90の場合、信号線(オレンジ)をGP15(ピン番号20)に、VCC(赤)を電池ボックスの+端子に、GND(茶)を電池ボックスの−端子とPicoのGND(ピン番号38)に接続します。前章同様、Pico本体の3.3Vや5V端子からサーボに直接給電すると動作が不安定になることがあるため、電池ボックスを使うのが無難です。

PWMでサーボを制御する仕組み

サーボは50Hz(周期20ms)のPWM信号によって角度が決まります。パルス幅の目安はSG90で次の通りです。

角度 パルス幅 duty_u16 の計算値
0° 500 µs 500 / 20000 × 65535 ≈ 1638
90° 1500 µs 1500 / 20000 × 65535 ≈ 4915
180° 2400 µs 2400 / 20000 × 65535 ≈ 7864
servo = PWM.new(15,  50,  0)

def set_angle(servo, angle)
  pulse_us = 500 + (angle * 1900.0 / 180).to_i  # 500〜2400 µs
  servo.duty_u16 = (pulse_us * 65535 / 20000).to_i
end

loop do
  set_angle(servo, 0)
  sleep 1
  set_angle(servo, 90)
  sleep 1
  set_angle(servo, 180)
  sleep 1
end

PWM.new(15, frequency: 50, ...) で50Hzに設定します。set_angle 関数では角度からパルス幅(µs)を計算し、20ms周期に対する割合をduty_u16の値に変換しています。50Hzという周期は前章のRgpio::Servoと同じです。

モータドライバの利用

DCモータを直接GPIOにつなぐと過電流でPicoが破損する恐れがあるため、モータドライバを間に入れます。ここでは TB6612 モジュールを使います。

用意するもの

接続

TB6612端子 接続先
AIN1 Pico W GP16(ピン番号21)
AIN2 Pico W GP17(ピン番号22)
PWMA Pico W GP18(ピン番号24)
STBY Pico W GP19(ピン番号25)
VCC Pico W 3.3V(ピン番号36)
GND Pico W GND(ピン番号38)
VM 外部電源(+)極
外部電源(-)極 Pico W GND(ピン番号38)と共通
AO1, AO2 DCモータ

GNDの共通化が重要です。 TB6612のGND・外部電源の(-)極・Pico WのGNDは必ず同じGNDラインに接続してください。

TB6612はAIN1/AIN2で回転方向を、PWMAでスピードを制御します。STBYピンをHIGHにしておかないとモータが動作しない点に注意してください。

AIN1 AIN2 動作
HIGH LOW 正転
LOW HIGH 逆転
LOW LOW 停止
class MotorDriver
  def initialize(ain1_pin, ain2_pin, pwm_pin, stby_pin)
    @ain1 = GPIO.new(ain1_pin, GPIO::OUT)
    @ain2 = GPIO.new(ain2_pin, GPIO::OUT)
    @pwm  = PWM.new(pwm_pin,  10000,  0)
    @stby = GPIO.new(stby_pin, GPIO::OUT)
    @stby.write(1)  # スタンバイ解除
  end

  def forward(speed = 0.5)
    @ain1.write(1)
    @ain2.write(0)
    @pwm.duty_u16 = (speed * 65535).to_i
  end

  def backward(speed = 0.5)
    @ain1.write(0)
    @ain2.write(1)
    @pwm.duty_u16 = (speed * 65535).to_i
  end

  def stop
    @ain1.write(0)
    @ain2.write(0)
    @pwm.duty_u16 = 0
  end
end

motor = MotorDriver.new(16, 17, 18, 19)

motor.forward(0.7)   # 70%のスピードで正転
sleep 3
motor.stop
sleep 0.5
motor.backward(0.5)  # 50%のスピードで逆転
sleep 3
motor.stop

MotorDriverクラスで制御ロジックをまとめています。speedは0.0〜1.0で指定します。クラスで抽象化することで、motor.forward(0.7)のように意図が明確なコードになります。前節のサーボと組み合わせれば、前進・後退・方向転換ができる簡単な自走ロボットに発展させることもできます。

PWMの周波数を10,000Hzに設定しているのは、スムーズな回転のためです。1,000Hzのような低い周波数では、モータのコイルがON/OFFのパルスを個別の力として受け取ってしまい、回転がぎこちなくなることがあります。周波数を十分に高くすると、パルスが速すぎてコイルには平均的な電流として伝わり、滑らかに回転します。TB6612は最大100kHzのPWMに対応しています。

ノイズ対策(USBが切れる場合)

モータのブラシが発生させる高周波ノイズがUSB接続を不安定にすることがあります。100nF(0.1µF)のセラミックコンデンサをモータの(+)端子にできるだけ近い位置で直接つなぐと、ノイズが抑えられて安定することがあります。セラミックコンデンサには極性はなく、向きを気にせず挿せます。

電源ライン側の電圧変動が疑われる場合は、電源付近に470µFの電解コンデンサ(こちらは極性があるので向きに注意)を挟むと改善することもあります。この手のノイズ・電圧対策は配線や部品の個体差に左右される部分が大きく、本書では深掘りせず、経験的に効果のあった対策の紹介にとどめます。

温度センサの利用

内蔵温度センサの利用

RP2040チップには内部温度センサが搭載されており、ADCチャンネル4(外部ピンには出ていない専用チャンネル)経由で読み取れます。prremoteではTemperatureクラスとして提供されており、配線不要で手軽に使えます。

temp = Temperature.new

puts "内蔵温度センサ — 10回計測:"
10.times do |i|
  c = temp.celsius
  f = temp.fahrenheit
  puts "#{i}: #{c} C  (#{f} F)"
  sleep 1.0
end

Temperature.newで内蔵センサのインスタンスを生成します。celsiusメソッドで摂氏、fahrenheitメソッドで華氏の温度を取得できます。なお、内蔵センサはチップ自体の発熱の影響を受けるため、周囲温度よりやや高めの値を示すことがあります。精度を求める用途では外付けセンサの利用を検討してください。

外付けセンサ(ADT7410)での計測

前章と同じ ADT7410(I2Cインタフェース)を使うこともできます。PicoのI2CはI2Cクラスで扱えます。接続はSDA/SCLの2本で完結し、SDA=GP4(ピン番号6)、SCL=GP5(ピン番号7)とつなぎます。

i2c = I2C.new( ,  4,  5,  400_000)

loop do
  data = i2c.read(0x48, 2, 0x00)   # レジスタ0x00から2バイト読み出し
  if data
    raw = ((data[0] << 8) | data[1]) >> 3  # 13ビット値
    raw -= 8192 if raw >= 4096             # 負の温度(2の補数)
    puts "#{raw / 16.0} C"
  else
    puts "read error"
  end
  sleep 1
end

I2C.newにはunit:(I2Cバス番号)、sda_pin:、scl_pin:を名前付き引数で渡します。i2c.read(addr, len, reg)は、レジスタアドレスregへの書き込みに続けてlenバイト読み出しをまとめて行います。16ビット(2バイト)のレジスタを読み出し、上位13ビットを取り出して温度に変換しています。

Raspberry Pi(Ruby/rgpio) Pico W(prremote/Ruby)
I2Cライブラリ Rgpio::I2C(require "rgpio") I2C(ビルトイン)
読み取り i2c.read_register(reg, len) i2c.read(addr, len, reg)
スキャン i2cdetect コマンド i2c.scan メソッド

接続されているI2Cデバイスのアドレスを確認するには以下のコードが使えます。

i2c = I2C.new( ,  4,  5)
puts i2c.scan.inspect
# [72]  →  0x48(10進72)が見つかった

LCDディスプレイ

I2C接続のLCDをI2Cクラスで制御します。ここでは前章(実践課題3)と同じ AQM0802A-RN-GBW(ST7032コントローラ内蔵、8文字×2行)を使います。ST7032はI2Cネイティブのコントローラのため、変換アダプタなしに直接I2Cで制御できます。

用意するもの

接続(LCDモジュール → Pico W)

LCD端子 Pico W接続先
VCC 3.3V(ピン番号36)
GND GND(ピン番号38)
SDA GP4(ピン番号6)
SCL GP5(ピン番号7)

I2Cアドレスは固定で0x3Eです。

LCDを操作するドライバクラス

ST7032にコマンドを送る際は、コントロールバイト(0x00=コマンドモード、0x40=データモード)に続けて値を1バイト送ります。

⚠️ 注意: 以下のLCDドライバは実機での動作確認が取れていません。特にsleepによる微小時間待機の精度はmruby/c上での実装によります。動作しない場合はウェイト時間の調整が必要になる可能性があります。

class LCD
  ADDR = 0x3E
  CMD  = 0x00  # コントロールバイト:コマンドモード
  DATA = 0x40  # コントロールバイト:データモード

  def initialize(i2c)
    @i2c = i2c
    init_lcd
  end

  def init_lcd
    sleep 0.04
    cmd(0x38)  # Function Set (8bit, 2line, 通常命令)
    cmd(0x39)  # Function Set (拡張命令セット選択)
    cmd(0x14)  # Internal OSC周波数設定
    cmd(0x70)  # コントラスト下位4bit設定
    cmd(0x56)  # 電源・コントラスト上位2bit設定
    cmd(0x6C)  # フォロワ回路制御
    sleep 0.2
    cmd(0x38)  # Function Set (通常命令セットに戻す)
    cmd(0x0C)  # ディスプレイON、カーソルOFF
    cmd(0x01)  # 画面クリア
    sleep 0.002
  end

  def cmd(c)
    @i2c.write(ADDR, CMD, c)
    sleep 0.001
  end

  def clear
    cmd(0x01)
    sleep 0.002
  end

  def move(col, row)
    offset = row == 0 ? 0x00 : 0x40
    cmd(0x80 | (offset + col))
  end

  def write(text)
    i = 0
    while i < text.size
      @i2c.write(ADDR, DATA, text[i])  # mruby/cではString#[]が整数バイトを返す
      i += 1
    end
  end
end

i2c = I2C.new( ,  4,  5)
lcd = LCD.new(i2c)

lcd.clear
lcd.move(0, 0)
lcd.write("Hi Pico!")
lcd.move(0, 1)
lcd.write("Ruby on!")

温度センサと組み合わせる

前節のADT7410と組み合わせると、1秒ごとに温度をLCDに表示するデジタル温度計が作れます。AQM0802Aは8文字×2行のため、表示内容を8文字以内に収めます。(LCDクラスの定義は上記の通り、省略)

# i2c / LCD の初期化は上記の通り

loop do
  data = i2c.read(0x48, 2, 0x00)
  if data
    raw = ((data[0] << 8) | data[1]) >> 3
    raw -= 8192 if raw >= 4096
    temp = "%.2f" % (raw / 16.0)
    lcd.clear
    lcd.move(0, 0)
    lcd.write("Temp:")
    lcd.move(0, 1)
    lcd.write("#{temp}C")
  end
  sleep 1
end

前章ではrgpioのRgpio::ST7032クラスを使いましたが、こちらではI2C転送の仕組みを理解した上でゼロから書くことで、コントローラの動作原理を学ぶことができます。

スタンドアロン動作

これまでのプログラムはprremote runでその都度実行していましたが、deployコマンドでスクリプトをフラッシュメモリに書き込むと、電源を入れるたびに自動的に起動します。

手順

  1. スクリプトを作成する(例:前節のLED点滅)
  2. deployコマンドで書き込む
prremote deploy blink.rb
# Compiling blink.rb...
# Deploying to flash...
# Deployed. Script will run automatically on next boot.
  1. USBケーブルを抜いてバッテリーをつなげば、Raspberry Piなしに単体で動作します

電源のつなぎ方

単三電池3本(1.5V × 3 = 4.5V)を VSYSピン(ピン番号39)とGND(ピン番号38) に接続します。PicoのVSYSは1.8〜5.5Vに対応しており、内部の3.3Vレギュレータが変換してくれます。USB電源バンクをMicro USBポートにつなぐのもシンプルな方法です。

電源 接続先 ポイント
単三電池 × 3 VSYS + GND 3本で4.5V、軽量
USB電源バンク Micro USB USB規格のまま接続

スクリプトを削除するには

prremote undeploy    # フラッシュのスクリプトを消去(自動実行を解除)

複数ファイルをまとめてデプロイ

クラス定義を別ファイルに分けてまとめてデプロイすることもできます。ファイルは左から順にコンパイルされ、前のファイルで定義したクラスを後のファイルで使えます。

prremote deploy motor_driver.rb main.rb

prremote コマンドまとめ

コマンド 動作
prremote run FILE コンパイル+即時実行(一回限り)
prremote watch FILE ファイル変更を監視して自動再実行
prremote deploy FILE フラッシュに書き込み(起動時に自動実行)
prremote undeploy フラッシュのスクリプトを消去
prremote eval "expr" 1行のRubyをPicoで即時実行
prremote list 接続デバイスを一覧表示
prremote reset 実行中のプログラムを停止(Ctrl+C相当)
prremote version gem・ランタイム・mrbcのバージョンを表示

「書いて試す段階ではwatch、完成したらdeploy」という使い分けが自然です。

PicoRuby(おまけ)

マイコンでRubyを動かす取り組みとしては、PicoRuby が先行しています。PicoRubyはワンチップマイコン向けのRuby処理系で、Raspberry Pi Pico向けには「R2P2」というシェルも用意されています。Picoにつないだシリアルターミナルから、Pico上で直接Rubyのコードを書いて試すことができます。

本章で使ったprremoteは、「Raspberry Pi上で書いたファイルを転送して実行する」という本書の作例の流れに合わせて、筆者が開発したツールです。PicoRubyの小型版(FemtoRuby)は、prremoteと同じmruby/cを実行エンジンに使っています。どちらもマイコンの上でRubyを動かすという点は同じなので、興味があればPicoRubyも試してみてください。

prremote PicoRuby(R2P2)
プログラムの書き方 Raspberry Piで書いたファイルを転送して実行 Pico上のシェルで直接書いて実行
書き込むファームウェア prremote install で書き込む リリースページのUF2ファイルを書き込む

2026年10月時点のPicoRubyのリリースページでは、Pico(Wi-Fiなし)向けとPico 2 W向けのファイルが配布されています(本章で使ったPico W向けのファイルはありません)。手持ちのボードに合ったファイルをダウンロードし、MicroPythonと同じようにBOOTSELモードで書き込みます。使い方の詳細は、PicoRubyのリポジトリにある説明を参照してください。

MicroPython(おまけ)

Pico Wのプログラミング言語として最も広く普及しているのがMicroPythonです。本章ではRuby(prremote)を主軸にしましたが、MicroPythonも非常に強力な選択肢であり、ライブラリの豊富さと情報量で大きな優位性を持ちます。

MicroPythonとは

MicroPythonはマイコン向けに設計された軽量のPython処理系です。Pythonの構文をほぼそのまま使えるため、前章まで学んできたPythonの知識をそのまま活かせます。コミュニティが非常に大きく、センサドライバや通信プロトコルのライブラリが豊富に揃っています。

セットアップ(Thonny)

Raspberry Pi OSにはThonnyというPython用IDEが標準インストールされており、MicroPythonのPico向け対応も組み込まれているため、追加インストールは不要です。

  1. Thonnyを起動し、画面右下のドロップダウンで「MicroPython 」を選びます
  2. 初回はThonny下部に「MicroPythonをインストール」リンクが表示されるのでクリックします
  3. ダイアログで対象デバイスがRaspberry Pi Pico Wになっていることを確認して「インストール」を実行します

MicroPythonのファームウェアが書き込まれたPicoは、USBを挿すと自動でThonnyから認識されます。

MicroPythonのコード例

本章で扱ったLED点滅をMicroPythonで書くと以下のようになります。

import machine
import time

led = machine.Pin("LED", machine.Pin.OUT)

while True:
    led.on()
    time.sleep(0.5)
    led.off()
    time.sleep(0.5)

Thonnyのコードエディタに入力して▶ボタンを押すだけで実行できます。停止は■ボタンです。スタンドアロン動作にするには、ファイルをPico上にmain.pyとして保存します(「File」→「Save as…」→「Raspberry Pi Pico」を選択)。

mpremote(MicroPython用CLIツール)

MicroPythonにもprremoteに対応するCLIツールmpremoteがあります。Thonnyを使わずにコマンドラインからファイル転送や実行が行えます。

python3 -m venv ~/venv
~/venv/bin/pip3 install mpremote
~/venv/bin/mpremote run blink.py             # ローカルファイルをPicoで実行
~/venv/bin/mpremote fs cp blink.py :blink.py # Picoにファイルをコピー
~/venv/bin/mpremote exec "import machine; machine.Pin('LED', machine.Pin.OUT).on()"

prremote(Ruby)との比較

prremote(Ruby) MicroPython
言語 Ruby(mruby/c) Python
実行ツール prremote run Thonny IDE または mpremote
コミュニティ 小さい(新興) 非常に大きい
サードパーティライブラリ 少ない 豊富
書き心地 Rubyらしいオブジェクト指向 Pythonらしいシンプルな構文

MicroPythonはライブラリの豊富さと参考情報の多さで有利です。インターネット上の情報量も圧倒的です。一方のprremoteは、RubyistがPicoに入門するための橋渡しとして魅力的であり、クラスやメソッドを使ったRubyらしい抽象化でコードを整理できます。「既存のPythonライブラリを使いたい」「情報量を重視したい」場合はMicroPythonを、「Rubyで書きたい」場合はprremoteを選ぶとよいでしょう。

実践課題5 Railsでウェブアプリ

この章ではRaspberry Piをウェブアプリケーションサーバとして利用する方法を解説します。ここで利用するフレームワークはRuby on Railsです。拙著、Ruby on Railsで作るウェブアプリケーション(Rails7対応版)の内容に近いものをRaspberry Pi環境向けに書き換えた構成になっています。Rails8に対応した新しいテキスト Dive into Ruby on Rails 2025 も公開しています。

まずは必要なパッケージを導入します。

sudo apt-get -y install rbenv ruby-dev libyaml-dev libpq-dev

rbenvはRubyの実行環境を管理するツールで、これを使ってrubyをインストールします。aptで入るruby-buildは定義が古く、最新のRubyをインストールできないので、一部分(ruby-buildプラグイン)だけ新しくします(実践課題4でRuby 4.0.7をインストール済みの場合、以下のruby-buildの取得からRubyのインストール、rbenv initの設定までは不要です)。

sudo apt remove ruby-build
mkdir -p "$(rbenv root)"/plugins
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
rbenv install 4.0.7
rbenv global 4.0.7
rbenv init  # ここに出てくる指示に従って設定ファイルを書き換えます

(rbenv init の案内が難しい方向け)以下のコマンドでインストール可能です。実行後に、今回インストールしたバージョン(4.0.7)が選択されていることを確認してください。

echo 'eval "$(rbenv init -)"' >> ~/.bashrc
. ~/.bashrc
rbenv versions  # systemではなく、4.0.7が選択されていることを確認

続けて、railsをインストールします。

gem update --system
gem install pg rails -N
rbenv rehash

それぞれのコマンドを起動してバージョンを確認してください。本書では以下の構成で解説を進めますが、マイナーバージョン(末尾の数値)が異なっても、大抵のケースでは問題はないはずです。

ruby -v   # ruby 4.0.7 (2026-09-15..
rails -v  # Rails 8.1.4

準備ができたら以下のコマンドで新しいプロジェクト(rails01-rooms)を作成し、その中に移動(cd)してください。

rails new rails01-rooms
cd rails01-rooms

lsコマンドやFile Managerで中身をのぞいてみてください。大量のファイルとディレクトリが作成されているはずです。この後、使うものを抜粋して掲載します。

以下のコマンドでウェブサーバを起動します。

bundle exec rails server

Raspberry Pi上でブラウザを起動して以下のアドレスにアクセスし、以下の画面が見えたらここまで順調です!

http://localhost:3000

rails

サーバを終了したい場合は「Ctrl+C」を入力します。

VNCなどでリモート接続して開発している場合、他のホストから動きを確認したい場合は、config/environments/development.rb に以下の設定を書き足します。

  config.hosts << `hostname`.strip+".local:3000"

その後、-bオプションを付けてサーバを起動します。sはserverの短縮形で、頭文字が一意なコマンドはこのように省略して実行することも可能です。

bundle exec rails s -b 0.0.0.0

サーバが起動している間は他のコマンドを入力することができませんので、いったんここまでできたら、新しいターミナルウィンドウを開いて、こちらはそのまま(起動した状態)にしておくと開発がスムーズになります。ただし、(config以下のファイルなど)一部の設定はサーバの起動時にしか読み込まれませんので、変更内容によっては、サーバの再起動が必要になります(Ctrl+Cで終了、上記コマンドで起動しなおします)。

足場づくり - scaffold

準備が整ったところで、さっそくモデル(データを保存する入れ物のようなもの)を作っていきましょう。今回は会議室の利用状況を把握するための、Roomというモデルを定義してみます。モデルを作るだけでは何も操作ができないので、あわせて 表示や更新、削除も行えるようにします。railsにはscaffold(足場)という、これらをまとめて自動生成する機能があります。ちなみに、この組み合わせは頻繁に使われるので、作成(create)、表示(read)、更新(update)、削除(delete)の頭文字を集めてCRUDと呼ばれることがあります。

以下のコマンドでscaffoldを実行します。gはgenerateの頭文字です。

bundle exec rails g scaffold room name occupied:boolean

大量のファイルが生成されていると思います。データベースにこのモデルの実データを格納するためのテーブルを作成するためのスクリプトも用意されます。ややこしいですね(;

スクリプトは以下のコマンドで実行できます(db:createは初回のみ)。

bundle exec rails db:create db:migrate

実際には、上記は実行し忘れることも結構あって、そのままサーバを立ち上げると、エラーが出ます。その場合、エラー画面の中ほどに「Run pending migrations」というボタンが出てきますので、これをクリックすることでも上記と同じ処理が実行されてテーブルが生成されます。

以下のアドレスにアクセスして、「New room」リンクから新しい部屋情報を登録できることを確認してください。

http://raspi5.local:3000/rooms または http://localhost:3000/rooms

rails

窓口づくり - api

次に、入退室の状態変更を外部から受け付けるためのAPIを作成します。

部屋の名前をキーにして指定できるようにしたいので、名前が一意になるようにモデルに制約を追加します。

app/models/room.rb を編集して以下の1行を足してください。

  validates :name, uniqueness: true

config/routes.rb にはAPIのパスを定義します。これは定義内の先頭(2行目)に書いてください(resources文の下に書くと意図したように動きません)。

get 'rooms/update-occupation', to: 'rooms#update_occupation'

コントローラ(app/controllers/rooms_controller.rb)に状態変更のためのコードを記述します。nameとoccupiedの2つのパラメータを受け取って、それを元にRoomモデルの更新をするメソッドです。他のアクション(show, new, .. destroy など)と同列に記述してください(privateの下に書くとアクセスできません)。

  def update_occupation
    name = params[:name]
    occupied = "true" == params[:occupied]
    logger.debug("name=#{name} occupied=#{occupied}")
    r = Room.find_by(name: name)
    if r.nil?
      render plain: "room not found\n"
      return
    end
    r.occupied = occupied
    r.save!
    render plain: "accepted\n"
  end

例えば、以下のようなコマンドで「Room A」を「使用中」に更新することが可能です。「OSのインストール」の節の設定通りにイメージを作っている場合、raspi5.local というホスト名でアクセス可能です。

curl http://(ホスト名).local:3000/rooms/update-occupation -XGET -d 'name=Room A&occupied=true'

コマンドの実行後に「accepted」と応答があれば成功です。パラメータの値を変えて他の部屋の状態も変更可能か確認してみましょう。

この例は今後の拡張機能を開発する際にも有用なので、app/views/rooms/show.html.erb にも書き込んでおきましょう。ホスト名と部屋名を変数にして動的に取得することで、実際に動作するコマンド例を生成することが可能です。

<p>How to update the occupation of a room using curl:</p>
<pre>
curl http://<%= request.server_name %>:3000/rooms/update-occupation -XGET -d 'name=<%= @room.name %>&occupied=true'
</pre>

飾りつけ - bootstrap

ここまでの手順で基本的な動作はできたので、見栄えをちょっと良くしてみましょう。ここではBootstrapというフレームワーク(部品のセット)を利用します。詳しい解説や作例は公式サイトを確認してください。ネットで色々見つかるかもしれませんが、バージョンごとに使えるクラス名がちょっとずつ異なるため、できる限り公式サイトの例を参考にした方がスムーズかと思います。

Bootstrap公式ドキュメント(Get started)

本書では執筆時点最新の安定版である5.3を利用しています。

app/views/layouts/application.html.erb に以下のように、headタグの最後尾にlinkタグを、bodyタグの最後尾にscriptタグを追加します。CDNという仕組みを利用しているので、テスト中もインターネット接続が必要な点に留意してください。閉鎖環境で使う場合は、それぞれ必要なファイルをダウンロードして読み込む必要があります。

:
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" 
    integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
  <%= stylesheet_link_tag "application" %>
</head>
<body>
  :
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>

準備ができたらスタイルを適用したい要素に適切なクラスをつけていくことで、見栄えを調整することができます。例えば、以下は app/views/rooms/index.html.erb 内の新規作成リンクをボタン風に表示する例です。

<%= link_to "New room".html_safe, new_room_path, class: "btn btn-light" %>

Bootstrap iconsでは、アイコンも多数提供されており、それらを利用することも可能です。

<%= link_to "<i class='bi bi-plus-square'></i> New room".html_safe, new_room_path, class: "btn btn-light" %>
bootstrap button

containerも利用しましょう。ページ内のコンテンツの幅を限定して大きな画面でも見やすくレイアウトする助けになります。ここでは触れませんが、レスポンシブなウェブサイトを作る際にも重宝する仕組みです。 app/views/layouts/application.html.erb の body内にある yield を以下のように div タグで囲みます。

:
<body>
  <div class="container">
    <%= yield %>
  </div>
  :

続けて、 app/views/rooms/index.html.erb の <div id="rooms"> .. </div> タグ以下を以下のようなテーブルで置き換えます。

<table id="rooms" class="table">
  <% @rooms.each do |room| %>
    <tr>
      <td><%= room.name %></td>
      <td>
        <% if room.occupied %>
          <div class="alert alert-danger">occupied</div>
        <% else %>
          <div class="alert alert-success">vacant</div>
        <% end %>
      </td>
      <td>
      <%= link_to "Show this room", room %>
      </td>
    </tr>
  <% end %>
</table>
index with bootstrap style

最後に、show.html.erb のボタンにもスタイルをつけてみましょう。mt-2 というクラスは要素の余白をコントロールするためのもので、この場合、 margin t op を5段階中 2 番目のサイズ確保するという意味になります。他にも pb-5 = padding bottom 5、とか mx-3 = margin x axis などなどさまざまな指定が可能です。参考リンク: margin and padding

  <%= link_to "Edit this room", edit_room_path(@room), class: "btn btn-light" %>
  <%= link_to "Back to rooms", rooms_path, class: "btn btn-light" %>

  <%= button_to "Destroy this room", @room, method: :delete, class: "btn btn-danger mt-2" %>
show page with bootstrap style

その他、レイアウトの支援をしてくれるrowやcolumnの概念、クラス名で余白を制御する機能などなど、ウェブアプリの機能を実装する上で役にたつものがたくさん揃っています。

公式サイトを参考にしながら、他のページにもスタイルを適用してみてください。

Bootstrap公式ドキュメント(Get started)

ちょっと今風に - actioncable

最後におまけで、ActionCableをこのアプリに導入してみます。ここまで見てきたウェブアプリは基本的に一つのリクエスト(Webページを開くという動作)に対して、一つの結果しか返さない単方向の仕組みでしたが、この仕組みを使うと、双方向のやり取りが可能になります。つまり、部屋の使用状態がリアルタイムに表示されるようにすることが可能です。

まず、notificationという名前のチャネルを作成します。

rails g channel notification

app/channels/notification_channel.rb を開いて、stream_fromを以下のように書き換えます。

stream_from "notification_channel"

リアルタイムに更新を通知したい対象、今回は、 app/controllers/rooms_controller.rb の update_occupation アクションの最後に以下の行を追加します(今回、contentは使用していませんが、メソッドの実装に合わせてRoomモデルのインスタンスであるrを渡しています)。

ActionCable.server.broadcast "notification_channel", {content: r}

通知を受け取った際に行いたい処理を app/javascript/channels/notification_channel.js の received メソッド内に記述します。パフォーマンスの観点からは、更新が必要な箇所だけ書き換える方が望ましいですが、今回はページ全体をリロードしています。

location.reload();

Roomの一覧ページを開いたまま、前節のAPI経由(=curlコマンドの実行)で状態を変更してみてください。ページを更新しなくても、APIからの変更があったタイミングで表示が変化することを確認できます。

rails action cable

運用を考慮した構成にする

RailsにはPumaというWebサーバが標準で組み込まれていて、上記で紹介した rails server コマンドで簡単に立ち上げることができます。 しかし、この方法だと24時間稼働させ続けるサービスではちょっと不便です(何かあるたびに誰かが起動しなければいけません)。

そこで、公式サイトでも紹介されている方法でサーバを標準のサービスとして登録し、自動的に起動するようにします。

まず /etc/systemd/system/puma-myapp.service を以下の内容で作成します。以下はrootユーザでの作成、実行が必要です。操作を間違えるとOS自体の動作がおかしくなることがありますので、慎重に取り扱ってください。 WorkingDirectory と ExecStart 内のパスはご自身の環境に合わせて書き換えてください。

[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
Type=notify
WatchdogSec=10

User=piuser
WorkingDirectory=/var/www/myapp/current
Environment="RAILS_ENV=development"

ExecStart=rbenv exec bundle exec puma -C config/puma.rb

Restart=always

[Install]
WantedBy=multi-user.target

開発用のサーバが起動していると失敗しますので、停止済みか確認した上で、以下のコマンドを実行します。

sudo systemctl daemon-reload
sudo systemctl enable puma-myapp
sudo systemctl start puma-myapp

うまくいけば、サーバを再起動した後もウェブアプリが稼働していることを確認できるはずです。 プロセスがずっと起動した状態になっているため、構成を変更した後は以下の要領で再起動が必要です。 restartの代わりにstopを指定して停止すれば rails server も再び利用できます。

sudo systemctl restart puma-myapp

当初はRails本と同じく、Phusion Passengerを使ってApacheと連携させる方式を紹介する予定でしたが、執筆時点ではRaspberry Pi上で最新版のパッケージが提供されておらず、ビルドも困難であったため、Pumaを直接デーモン化するこの形を採用しています。

発展課題例

この章で作成したRailsプロジェクトは以下のリポジトリに公開しています。もしここまでの動作で不明なところがあれば、こちらを参考にして間違いがないか探してみてください。

実際に利用する場面を想定して、あるWi-Fi環境下に設置した時に、ユーザがどのようにアクセスするのかという実用面から含めてアプリの流れを考えてみましょう。操作手順書のようなものを先に作ってしまって、それに合わせて実装を拡張するような流れも良いかもしれません。

障害対応、バックアップはどうしますか?1~2年というスパンで動かすことを考えた時にどんなメンテナンスが必要になるでしょうか?ほんの十数年前までは、中小企業の基幹システムと言えば、この程度のスペックのサーバを事務所の真ん中にドンと置いて大事に使う、というものが主流でした(今は大部分クラウドに移ったと思いますが)。

実践課題6 OpenCVとYOLOで画像処理

この章では、OpenCVという画像処理ライブラリとUltralyticsという機械学習による画像分類のためのライブラリの利用方法を紹介します。まずは、必要な環境を準備します(opencvは既に1章でも触れています)。

環境の準備

sudo apt install python3-opencv python3-full
cd
python3 -m venv ./venv
./venv/bin/pip3 install torch torchvision --index-url https://download.pytorch.org/whl/cpu
./venv/bin/pip3 install ultralytics

今までと異なるのは、3行目でvenvという仮想環境を作っている部分です。Raspberry Pi OSの提供するパッケージに含まれないライブラリ(今回はultralytics)をインストールするために推奨されている方法です。上記のコマンドを実行するとHOME直下にvenvというディレクトリが作成され、その中にpython関連のコマンドが配置されます。

4行目では、ultralyticsが使う機械学習ライブラリのPyTorchを、先にCPU版でインストールしています。--index-urlは、pipがパッケージを探しに行くダウンロード元の指定です。指定しないと、Raspberry Piでは使わないNVIDIA GPU向けのライブラリ(合計で数GB)まで一緒にダウンロードされてしまい、時間がかかったり、途中で失敗したりすることがあります。PyTorchの開発元が配布しているCPU版を指定して入れておくことで、これを避けられます。

まずは、OpenCVの機能を使って、デスクトップ上にカメラの映像を表示するシンプルなプログラムです。 名前は video.py として保存してください。

必要なもの:USB接続のウェブカメラ

import cv2

cv2.namedWindow('cv2-captured-image')
cam = cv2.VideoCapture(0)

while True:
    ret, image = cam.read()
    image = cv2.resize(image,None,fx=0.5,fy=0.5)
    cv2.putText(image,"Press any key to exit",(0,24),cv2.FONT_HERSHEY_SIMPLEX,1.0,(255,0,0))
    cv2.imshow('cv2-captured-image', image)
    if cv2.waitKey(10) != -1:
        break
cam.release()
cv2.destroyAllWindows()

この章ではvenvを使いますので、今まではpythonまたはpython3とだけ書いていた部分を、以下のように指定する必要があります(以降のスクリプトも同様です)。

~/venv/bin/python3 video.py

実行後、新しいウィンドウが開いてカメラからの映像が見えたらOKです。プログラム中にも書いているように、何かキーを押すと終了します。

YOLO(物体検出、ポーズ推定)を試してみる

次にYOLOによる物体検出の結果を書き込んでみましょう。ファイル名はyolo1-detect.pyです。

import cv2

if __name__ == '__main__':
    cv2.namedWindow('find-on-video')
    cam = cv2.VideoCapture(0)

    from ultralytics import YOLO
    model = YOLO('yolov8n.pt')  # yolov8n-pose.pt
    while True:
        ret, image = cam.read()
        results = model(image)
        cv2.imshow('find-on-video',results[0].plot())
        k = cv2.waitKey(1)
        if k != -1:
            break

    cam.release()
    cv2.destroyAllWindows()

以下のように画面に映った物体を検出して名前付きの枠で囲ってくれます。横にある数字は、どれくらいの確度でその物体であるかを表しています。

yolo

上記のプログラムで読み込んでいた’yolov8n.pt’というモデルを’yolov8n-pose.pt’に変更すると、画面に映った人物の姿勢を推定することができるようになります。

デフォルトでは手首、肩、足などのポイントとそれを結ぶ線分が描画されますが、各ポイントを取り出して値を利用することもできます。下の例(yolo2-pose.py)では、左右の手首の情報だけ取り出して、その位置にそれぞれ青と赤の円を描いています。

import cv2

if __name__ == '__main__':
    cv2.namedWindow('find-on-video')
    cam = cv2.VideoCapture(0)

    from ultralytics import YOLO
    model = YOLO('yolov8n-pose.pt')
    while True:
        ret, image = cam.read()
        results = model(image)
        #cv2.imshow('find-on-video',results[0].plot())
        xy = results[0].keypoints.xy
        if len(xy) > 0 and len(xy[0]) == 17:
            l = xy[0][9] # left wrist
            r = xy[0][10] # right wrist
            cv2.circle(image,(int(l[0]),int(l[1])),10,(255,0,0),3) # blue
            cv2.circle(image,(int(r[0]),int(r[1])),10,(0,0,255),3) # red
        cv2.imshow('find-on-video',image)
        k = cv2.waitKey(1)
        if k != -1:
            break

    cam.release()
    cv2.destroyAllWindows()
yolo

上記のサンプルをRaspberry Piで実行すると1フレームの処理に500ms近くかかります。また、画面内に複数の人がいる場合、ランダムな1名分しか描画されない問題がありました。そこで、これらを改善する例を紹介します。まず、YOLOに読み込ませるモデルの形式をNCNNというものに変更します。

~/venv/bin/pip3 install ncnn

以下の手順でモデルの変換が可能です。ついでにyoloも8から11にバージョンアップしています(vがとれている点にご注意ください)。

from ultralytics import YOLO

model = YOLO("yolo11n-pose.pt")
model.export(format="ncnn")

プログラムは以下のようになります。モデル名として指定しているのは、上記の手順で作成した場合のディレクトリ名です(同名のディレクトリが存在するか確認してください)。

keypoints.xyをイテレート(for文で繰り返し処理)し、各要素をそれぞれ描画しています。これで、処理もスムーズかつ複数人が映っていても問題なく処理ができるようになっているはずです。1フレームの処理時間も150ms程度に短縮されています。

import cv2

if __name__ == '__main__':
    cv2.namedWindow('yolo11n')
    cam = cv2.VideoCapture(0)

    from ultralytics import YOLO
    model = YOLO('yolo11n-pose_ncnn_model')
    while True:
        ret, image = cam.read()
        #image = cv2.resize(image,None,fx=0.5,fy=0.5)
        results = model(image)
        m = "%d person" % (len(results[0].keypoints.xy))
        font = cv2.FONT_HERSHEY_SIMPLEX
        cv2.putText(image,m,(10,460),font,1,(255,255,255),1,cv2.LINE_AA)
        for xy in results[0].keypoints.xy:
            if len(xy) != 17: continue
            l = xy[9] # left wrist
            r = xy[10] # right wrist
            cv2.circle(image,(int(l[0]),int(l[1])),6,(255,0,0),3) # blue
            cv2.circle(image,(int(r[0]),int(r[1])),6,(0,0,255),3) # red
        cv2.imshow('yolo11n',image)
        k = cv2.waitKey(1)
        if k != -1:
            break

    cam.release()
    cv2.destroyAllWindows()

OpenCVで画像を処理する

先にハイレベルなライブラリを紹介してしまいましたが、ここでコンピュータビジョンの先駆的なライブラリの紹介に戻りたいと思います。AIによる画像の認識や処理に比べると地味な領域ではありますが、例えば工場の生産ラインのような条件が限られた限定的な世界では、こうした伝統的な手法で画像処理をした上で活用する方法がまだまだ有効と考えています。

まずは下準備として以下の共通関数スクリプトファイル(common.py)とサンプル画像、結果画像を保存するフォルダを用意してください。common.pyの中身は以下のようになります(ちょっと長いです…)。

import numpy
import cv2

SRCDIR = "samples"
DSTDIR = "results"

# Read an image from file.
# 1: IMREAD_COLOR, 0: IMREAD_GRAYSCALE, -1: IMREAD_UNCHANGED
def imread(name,flag=0):
    return cv2.imread(SRCDIR+"/"+name,flag)

# Print properties of the image.
def print_props(prefix,img):
    if img is None: return
    elif len(img.shape) == 2:
        # grayscale
        h,w = img.shape
        ch = 1
    else:
        h,w,ch = img.shape
    s = img.size
    dt = img.dtype
    print(prefix+" width: %d, height: %d, channel: %d, size: %d, dtype: %s" \
        % (w,h,ch,s,str(dt)))

# Display images and save the result.
def imshow(name, img1, img2):
    print_props("src:",img1)
    print_props("rst:",img2)
    max_height = max(img1.shape[0],img2.shape[0])
    total_width = img1.shape[1] + img2.shape[1]
    img = numpy.zeros((max_height,total_width,3),numpy.uint8)
    if len(img1.shape) == 2:
        img1 = cv2.cvtColor(img1,cv2.COLOR_GRAY2BGR)
    if len(img2.shape) == 2:
        img2 = cv2.cvtColor(img2,cv2.COLOR_GRAY2BGR)    

    img[0:img1.shape[0], 0:img1.shape[1]] = img1
    img[0:img2.shape[0], img1.shape[1]:img.shape[1]] = img2

    # img = cv2.add(img1,img2)
    cv2.imshow(name,img)
    cv2.waitKey(0)
    cv2.imwrite(DSTDIR+"/"+name+".jpg",img)
    print("Saved the image to: "+DSTDIR+"/"+name+".jpg")
    cv2.destroyAllWindows()

ファイルとディレクトリは以下のような関係になります。

ここから先は、保存するファイル名 : 処理の説明に続けて実際のコードを記載します。サンプルのsample.jpgには、各自で撮影した写真などを用意してください。本書の結果サンプル用には以下の画像を使用しました。

sample

opencv1-blur.py : まずはシンプルな例です。画像をグレースケールで読み込んでぼかし(blur)フィルタを適用した結果を表示/保存します。

import cv2
import common

img1 = common.imread('sample.jpg',0)
img2 = cv2.blur(img1,(5,5))

common.imshow('blur',img1,img2)

以下のように実行します。

python3 opencv1-blur.py
result of blur

何かキーを押すとウィンドウが閉じられ、表示された画像と同じものがresultsフォルダ内に保存されます。このとき、キーを押さずにマウスでウィンドウの閉じるボタンを押して終了してしまうと、pythonのプロセスが残ってしまうので注意してください(終了するにはkillコマンドを使う必要があります)。

opencv2-contours.py : 画像の輪郭を抽出するサンプルです。まず、二値化処理をした画像から輪郭(白と黒の境目にあたる部分の線分の集合)を取り出し、それを別の画像(img2)に描画しています。複雑ですが、古典的な物体検出の際にはよく使われるアプローチです。thresholdやfindContoursのパラメータ、あるいは読み込む画像を変えながらどのように結果が変化するか確認してみると理解が進むかもしれません。

shapes
import cv2
import common
import numpy

img1 = common.imread('shapes.jpg',0)
ret,img1cp = cv2.threshold(img1,127,255,cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(img1cp,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
img2 = numpy.zeros(img1.shape,numpy.uint8)
cv2.drawContours(img2, contours, -1, 255, 1)
print("number of contours = %d" % (len(contours)))
for i in range(0,len(contours)):
    c = contours[i]
    f = cv2.FONT_HERSHEY_SIMPLEX
    a = cv2.contourArea(c)
    l = cv2.arcLength(c,True)
    m = "%d" % (i)
    print("%d: len=%d, arcLen=%d, area=%d" % (i,len(c),l,a))
    p = tuple(c[0][0])
    cv2.putText(img2,m,p,f,0.4,(255),1,cv2.LINE_AA)

common.imshow('contours',img1,img2)

# See below for more details:
# http://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html#contour-features
result of contours

opencv3-drawing.py : 図形を描画するメソッドの例です。画像処理がメインのライブラリなので、特に目新しい物はないのですが、ちょっとした時に役にたったりするので紹介しています。続く2つ(colors,pixel)も同様で、ここで深く理解する必要はありませんが、こういう機能もあると知っておくとデバッグなどで役に立つことがあるかもしれません。左上が原点で、座標は(X,Y)形式で指定します。

import numpy
import cv2
import common

img = numpy.zeros((512,512,3),numpy.uint8)

cv2.line(img,(0,0),(511,511),(255,0,0),5)
cv2.rectangle(img,(384,0),(510,128),(0,255,0),3)
cv2.circle(img,(447,63),63,(0,0,255),-1)
cv2.ellipse(img,(256,256),(100,50),0,0,180,255,-1)
pts = numpy.array([[10,5],[20,30],[70,20],[50,10]],numpy.int32)
pts = pts.reshape((-1,1,2))
cv2.polylines(img,[pts],True,(0,255,255))  # use fillPoly to fill the shape.
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(img,'OpenCV',(10,500),font,4,(255,255,255),2,cv2.LINE_AA)

cv2.imshow('drawing',img)
cv2.imwrite(common.DSTDIR+"/drawing.jpg",img)
cv2.waitKey(0)
result of drawing

opencv4-colors.py : 色を扱うサンプルです。NumPy 2.0でitemsetメソッドが廃止されたため、画素への書き込みはimg1[y,x] = ...の形で行っています(以前の書き方をコメントで残しています)。

import cv2
import numpy
import common

img1 = numpy.zeros((60,360,3),numpy.uint8)
img2 = img1
h,w,_ = img1.shape

for y in range(0,h):
    for x in range(0,w):
        # NumPy 1.x: img1.itemset((y,x,0),x/2)
        # NumPy 1.x: img1.itemset((y,x,1),255)
        # NumPy 1.x: img1.itemset((y,x,2),255)
        img1[y,x] = (x//2,255,255)
img2 = cv2.cvtColor(img1,cv2.COLOR_HSV2BGR)

common.imshow('colors',img1,img2)
result of colors

opencv5-pixel.py : グレースケールイメージの任意の箇所にピクセル単位で描画をするサンプルです。

import cv2
import common

img1 = common.imread('sample.jpg')
img2 = img1.copy()
v = img2.item(10,20)
print(v)
for y in range(10,20):
    for x in range(20,40):
        # NumPy 1.x: img2.itemset((y,x),(x+y)*4)
        img2[y,x] = (x+y)*4
v = img2.item(10,20)
print(v)
v = img2.item(19,39)
print(v)

common.imshow('pixel',img1,img2)
result of pixel

opencv6-roi.py : ROIという概念は色んな場面で登場します。CとPythonでは扱い方が違うので最初は戸惑うかもしれません。

import cv2
import common

img1 = common.imread('sample.jpg',0)
img2 = img1[100:180, 90:150]  
cv2.rectangle(img2,(0,0),(59,79),255,3)

common.imshow('roi',img1,img2)
result of roi

opencv7-split.py : ROI同様、画像のレイヤー(RGB,HSVなど)の考え方も慣れが必要です。

import cv2
import common

img1 = common.imread('sample.jpg',1)
img2 = img1.copy()
img2[:,:,2] = 0 # Make all red pixels to zero.

# b,g,r = cv2.split(img1)
# img2 = cv2.merge((b,g,r)) # Same image
# img2 = b # Grayscale image using red pixels

common.imshow('split',img1,img2)

# See below for details:
# http://docs.opencv.org/3.0-beta/doc/py_tutorials/py_core/py_basic_ops/py_basic_ops.html#splitting-and-merging-image-channels
result of split

opencv8-thresh.py : 輪郭抽出のサンプルでも出てきた二値化処理のサンプルです。THRESH_BINARYは最もシンプルなアルゴリズムですが、このほかにもいくつかのアルゴリズムが利用できます。THRESH_OTSU が(筆者の経験の範囲では)多様な場面で”いい感じ”の処理をしてくれます。

import cv2
import common

img1 = common.imread('sample.jpg',0)
ret,img2 = cv2.threshold(img1,127,255,cv2.THRESH_BINARY)

common.imshow('thresh',img1,img2)
# https://docs.opencv.org/4.x/d7/d4d/tutorial_py_thresholding.html
result of threshold

発展課題例

OpenCVの公式ドキュメントを見ると、他にもまだまだたくさんのフィルタが存在しています。これらを使って、カメラの画像をリアルタイムに加工するアプリケーションを作ってみましょう。Yoloと組み合わせて、人物の特定の動作に合わせてフィルタを適用したり、画像を加工するなどの組み合わせでも遊べそうです。自由に課題を決めて何か作ってみてください。

実践課題7 人物検知カメラを作る

YOLOによる人物検知システムが撮影した画像をブラウザ経由で閲覧、統計を見ることができるアプリを開発します。 以下の項目をあらかじめ理解していることを、前提として記載しています(HTMLの基本以外は本書内にも解説があります)。

この章では、前章で学んだYOLOを使って人物を検知し、検知した際に画像を自動保存するシステムを構築します。さらに、保存された画像をブラウザで閲覧できるようにSinatraというRubyのウェブフレームワークを使ってウェブアプリケーションを作成します。

以下のような機能を実装します。

完成すると以下のような画面で画像を閲覧できるようになります。時間帯別の検知回数を棒グラフで表示し、最新の検知画像とスナップショット画像を確認できます。

YOLOによる人物検知システムの作成

まずは前章で学んだYOLOを使って、人物を検知したときに画像を保存するプログラムを作成します。以下の準備が必要です(前章で既に実施済みの場合はスキップしてください)。

sudo apt install python3-opencv python3-full
cd
python3 -m venv ./venv
./venv/bin/pip3 install torch torchvision --index-url https://download.pytorch.org/whl/cpu
./venv/bin/pip3 install ultralytics ncnn pnnx

YOLOのモデルを準備します。前章でNCNN形式に変換したモデルを使用すると処理速度が向上します。

cd
mkdir -p yolo_detection
cd yolo_detection

Python対話シェルを起動してモデルを準備します。

~/venv/bin/python3

以下のコードを実行してモデルをエクスポートします。

from ultralytics import YOLO
model = YOLO("yolo11n.pt")
model.export(format="ncnn")
exit()

これで yolo11n_ncnn_model というディレクトリが作成されます。

次に、人物検知と画像保存のプログラム detect_person.py を作成します。

nano detect_person.py

以下の内容を記述します。

import cv2
import os
from datetime import datetime
from ultralytics import YOLO

# 画像保存ディレクトリの設定
CAPTURE_DIR = os.path.expanduser("~/yolo_captures")
MOTION_DIR = os.path.join(CAPTURE_DIR, "motions")
SNAPSHOT_DIR = os.path.join(CAPTURE_DIR, "snapshots")

# ディレクトリを作成
os.makedirs(MOTION_DIR, exist_ok=True)
os.makedirs(SNAPSHOT_DIR, exist_ok=True)

# YOLOモデルのロード
model = YOLO('yolo11n_ncnn_model')

# カメラの初期化
cam = cv2.VideoCapture(0)
if not cam.isOpened():
    print("Error: Cannot open camera")
    exit(1)

# スナップショット用のタイマー
last_snapshot_time = datetime.now()
snapshot_interval = 3600  # 秒(1時間)

# 検知状態管理
detecting = False
best_confidence = 0.0
best_frame = None
best_result = None
no_detection_count = 0
max_no_detection = 5  # 5フレーム検知されなかったら検知終了

# 人数変化の安定性チェック用
confirmed_person_count = 0  # 確定した人数
candidate_person_count = 0  # 候補人数
candidate_count_frames = 0  # 候補人数が連続して検出されたフレーム数
required_stable_frames = 3  # 人数変化を確定するのに必要な連続フレーム数

print("Person detection system started")
print(f"Motion images: {MOTION_DIR}")
print(f"Snapshots: {SNAPSHOT_DIR}")
print("Press Ctrl+C to exit")

try:
    while True:
        ret, image = cam.read()
        if not ret:
            print("Error: Cannot read frame")
            break
        
        # YOLOで物体検出
        results = model(image, verbose=False)
        
        # 人物の数と最大信頼度をカウント
        person_count = 0
        max_conf = 0.0
        
        for result in results:
            if result.boxes is not None:
                for box in result.boxes:
                    if int(box.cls[0]) == 0:  # クラス0 = person
                        conf = float(box.conf[0])
                        if conf < 0.65:
                            continue  # 信頼度が低い検出は無視
                        person_count += 1
                        if conf > max_conf:
                            max_conf = conf
        
        person_detected = person_count > 0
        
        # 人数の安定性をチェック
        if person_count == candidate_person_count:
            # 同じ人数が続いている
            candidate_count_frames += 1
        else:
            # 人数が変わった(候補をリセット)
            candidate_person_count = person_count
            candidate_count_frames = 1
        
        # 人数が安定して変化したかチェック
        if candidate_count_frames >= required_stable_frames:
            # 人数が確定した
            if candidate_person_count != confirmed_person_count and person_detected:
                # 確定した人数が変化した場合のみ保存
                timestamp = datetime.now().strftime("%y%m%d%H%M%S")
                filename = f"{timestamp}-{candidate_person_count}.jpg"
                filepath = os.path.join(MOTION_DIR, filename)
                
                annotated_image = results[0].plot()
                cv2.imwrite(filepath, annotated_image)
                print(f"Person count changed ({confirmed_person_count}->{candidate_person_count}): {filename}")
                
                confirmed_person_count = candidate_person_count
        
        # 検知状態の管理
        if person_detected:
            no_detection_count = 0
            
            if not detecting:
                # 新しい検知セッション開始
                detecting = True
                best_confidence = max_conf
                best_frame = image.copy()
                best_result = results[0]
                print("Detection started")
            else:
                # 検知継続中 - より良いフレームがあれば更新
                if max_conf > best_confidence:
                    best_confidence = max_conf
                    best_frame = image.copy()
                    best_result = results[0]
        else:
            if detecting:
                no_detection_count += 1
                # 一定フレーム検知されなかったら検知終了
                if no_detection_count >= max_no_detection:
                    # 最良のフレームを保存
                    timestamp = datetime.now().strftime("%y%m%d%H%M%S")
                    filename = f"{timestamp}-{confirmed_person_count}.jpg"
                    filepath = os.path.join(MOTION_DIR, filename)
                    
                    annotated_image = best_result.plot()
                    cv2.imwrite(filepath, annotated_image)
                    print(f"Person detected (conf: {best_confidence:.2f}): {filename}")
                    
                    # 状態リセット
                    detecting = False
                    best_confidence = 0.0
                    best_frame = None
                    best_result = None
                    no_detection_count = 0
                    # 人数もリセット
                    confirmed_person_count = 0
                    candidate_person_count = 0
                    candidate_count_frames = 0
        
        # 定期的にスナップショットを保存
        now = datetime.now()
        elapsed = (now - last_snapshot_time).total_seconds()
        if elapsed >= snapshot_interval:
            timestamp = now.strftime("%y%m%d%H%M%S")
            filename = f"{timestamp}-snapshot.jpg"
            filepath = os.path.join(SNAPSHOT_DIR, filename)
            cv2.imwrite(filepath, image)
            print(f"Snapshot saved: {filename}")
            last_snapshot_time = now
        
        # 処理速度の調整(0.5秒待機)
        cv2.waitKey(500)

except KeyboardInterrupt:
    print("\nTerminating...")
finally:
    cam.release()
    cv2.destroyAllWindows()

このプログラムは以下の動作をします。

  1. カメラから映像を取得
  2. YOLOで人物を検知し、人数をカウント
  3. 人数が3フレーム連続で同じ値になるまで待機(安定性チェック)
  4. 安定して人数が変化したとき(0→1、1→2、2→1など)に画像を保存(ファイル名の末尾には確定した人数が付く)
  5. 人物が検知されたら検知セッションを開始し、検知が続いている間は最も信頼度の高いフレームを記録
  6. 人物が検知されなくなったら(5フレーム連続で未検知)、検知セッション中の最良フレームをmotionsディレクトリに保存
  7. 1時間ごとにスナップショットをsnapshotsディレクトリに保存
  8. Ctrl+Cで終了するまで繰り返し

このメカニズムにより、以下のような利点があります。

例えば、1人がずっと滞留している場合でも、入室時に1枚(人数変化)、退室時に2枚(人数変化+検知セッション終了時の最良フレーム)が保存されます。一時的な検出ミス(1人→2人→1人のような瞬間的な誤検知)は無視されます。

プログラムを実行してみましょう。

~/venv/bin/python3 detect_person.py

カメラの前で動いてみて、人物が検知されることを確認してください。画像ファイルが~/yolo_captures/motions/に保存されていれば成功です。

画像表示ウェブサーバの準備

検知システムで保存された画像をブラウザで閲覧できるように、Webアプリケーションを作成します。今回は、シンプルなアプリを作るのに便利なSinatraというRubyのフレームワークを利用します。

まず、Sinatraをインストールします。実践課題5で導入したrbenvのRubyを使うため、sudoは付けません。

gem install sinatra rackup puma -N

アプリケーションを作る場所を用意します。webappというディレクトリの中にmain.rbというファイルを作成します。

cd ~/yolo_detection
mkdir -p webapp/views && cd webapp
nano main.rb

main.rbには以下のように記述してください。

require 'sinatra'

# 画像保存ディレクトリの設定
set , File.expand_path('~/yolo_captures')

get '/' do
  erb 
end

get '/motions' do
  @image_type = "motions"
  erb 
end

get '/snapshots' do
  @image_type = "snapshots"
  erb 
end

get '/activities' do
  erb 
end

続けて、views/index.erb というファイルを作成します。

nano views/index.erb

以下の内容を記述します。基本的なHTMLのタグの中に、<%= ... %>で囲まれた部分があります。これがerbと呼ばれる記法で、このファイルの拡張子にもなっています。<%= ... %>の中に、Rubyのコードを直接書くことができ、これを用いてウェブページに動的な要素を組み込みます。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>YOLO Person Detection</title>
    <style>
      body {
        font-family: Tahoma, sans-serif;
        margin: 20px;
      }
      .header {
        background-color: #f0f0f0;
        padding: 10px;
        margin-bottom: 20px;
      }
      div#activities, div#motions, div#snapshots {
        display: flex;
        overflow-x: scroll;
        gap: 10px;
        margin: 20px 0;
      }
      div#motions img, div#snapshots img {
        max-width: 320px;
        border: 1px solid #ccc;
      }
      h2 {
        clear: both;
      }
      .image-container {
        text-align: center;
      }
      .image-container small {
        display: block;
        color: #666;
      }
    </style>
  </head>
  <body>
    <div class="header">
      <h1>YOLO Person Detection System</h1>
      <%
        motion_dir = File.join(settings.public_folder, 'motions')
        snapshot_dir = File.join(settings.public_folder, 'snapshots')
        motion_count = Dir.exist?(motion_dir) ? Dir.children(motion_dir).count : 0
        snapshot_count = Dir.exist?(snapshot_dir) ? Dir.children(snapshot_dir).count : 0
      %>
      <p>
        <strong>検知画像:</strong> <%= motion_count %>枚 |
        <strong>スナップショット:</strong> <%= snapshot_count %>枚 |
        <strong>保存場所:</strong> <%= settings.public_folder %> |
        <%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %>
        <a href="/">Reload</a>
      </p>
    </div>
    
    <div id="activities"></div>
    
    <h2>検知画像 (Motions)</h2>
    <div id="motions"></div>
    
    <h2>スナップショット (Snapshots)</h2>
    <div id="snapshots"></div>
    
    <script>
      function load(id, path) {
        var r = new XMLHttpRequest();
        r.open("GET", path, true);
        r.onreadystatechange = function () {
          if (r.readyState != 4 || r.status != 200) return;
          document.getElementById(id).innerHTML = r.responseText;
        };
        r.send();
      }
      
      load("motions", "/motions");
      load("snapshots", "/snapshots");
      load("activities", "/activities");
    </script>
  </body>
</html>

次に、views/images.erb を作成して画像を表示します(motionsとsnapshotsの両方で使用)。

nano views/images.erb

以下の内容を記述します。

<%
  # @image_typeに応じてディレクトリとメッセージを設定
  image_dir = File.join(settings.public_folder, @image_type)
  title = @image_type == 'motions' ? '検知画像' : 'スナップショット'
  empty_message = @image_type == 'motions' ? '検知画像はまだありません' : 'スナップショットはまだありません'
  
  fnames = []
  if Dir.exist?(image_dir)
    fnames = Dir.children(image_dir)
      .select { |f| f.end_with?('.jpg') }
      .sort.reverse[0, 20]  # 最新20枚
  end
%>
<% if fnames.empty? %>
  <p><%= empty_message %></p>
<% else %>
  <% fnames.each do |f| %>
    <div class="image-container">
      <a href="/<%= @image_type %>/<%= f %>" target="_blank">
        <img src="/<%= @image_type %>/<%= f %>" alt="<%= f %>"/>
      </a>
      <small><%= f %></small>
    </div>
  <% end %>
<% end %>

このようにmotions.erbとsnapshots.erbの内容を1つのテンプレートに統合しました。main.rbで/motionsと/snapshotsの2つのルートから同じテンプレートを呼び出し、@image_type変数に応じてディレクトリ名とメッセージを動的に切り替えます。

最後に、時間帯別の検知回数グラフを表示する views/activities.erb を作成します。

nano views/activities.erb
<style>
  table.activities {
    border- collapse;
  }
  
  table.activities td {
    vertical- bottom;
    font- 9px;
     #ccc;
  }
  
  table.activities div {
     10px;
    background- LightSeaGreen;
  }
</style>

<table class="activities">
  <tr>
<%
  motion_dir = File.join(settings.public_folder, 'motions')
  frequencies = {}
  
  if Dir.exist?(motion_dir)
    # ファイル名から時刻を抽出して集計
    # ファイル名形式: 260312141530.jpg -> 2603121415 (YYMMDDHHmm) -> 26031214 (年月日時)
    frequencies = Dir.children(motion_dir)
      .select { |f| f.end_with?('.jpg') && !f.include?('snapshot') }
      .map { |f| f[0, 8].to_i }  # YYMMDDHHを取得
      .group_by(&:itself)
      .transform_values(&:count)
  end
  
  # グラフの高さを計算
  max_count = frequencies.values.max || 1
  h = (120 - 1) / max_count
  
  # 過去7日分の時間別データを表示
  t = Time.now
  h_offset = t.hour
  (24 * 7).times do
    i = t.strftime("%y%m%d%H").to_i
    count = frequencies[i] || 0
%>
    <td>
      <div class="activities-box" style="height: <%= count * h + 1 %>px"></div>
    </td>
<%
    t -= 3600  # 1時間前に戻る
  end
%>
  </tr>
  <tr>
    <% t = Time.now %>
    <td colspan="<%= h_offset %>"><%= t.strftime("%F") %></td>
    <%
      t -= 3600 * (h_offset + 1)
      6.times do
    %>
      <td colspan="24"><%= t.strftime("%F") %></td>
    <%
        t -= 3600 * 24
      end
    %>
  </tr>
</table>

これで必要なファイルがすべて揃いました。以下のコマンドでSinatraのWebサーバを起動します。

ruby main.rb

ブラウザから http://localhost:4567 にアクセスしてページが表示されることを確認してください。

他のホストからもアクセスできるようにする場合は、以下のようにバインドアドレスを指定します。

ruby main.rb -o 0.0.0.0

これで、Raspberry PiのIPアドレスを使って他のデバイスからもアクセスできるようになります(例: http://raspi5.local:4567)。

動作確認とテスト

システム全体の動作を確認してみましょう。

  1. まず、人物検知プログラムを起動します(別のターミナルで)。
cd ~/yolo_detection
~/venv/bin/python3 detect_person.py
  1. 次に、Webアプリを起動します(さらに別のターミナルで)。
cd ~/yolo_detection/webapp
ruby main.rb -o 0.0.0.0
  1. ブラウザでアクセスして動作を確認します

カメラの前で動いて人物検知が動作することを確認し、Webアプリで画像が表示されることを確認してください。画像が保存されると、グラフにも反映されます。

この仕組みを使って応用することも可能です。例えば、以下のような応用が考えられます。

時間帯別のグラフ描画では、ファイル名から日時情報を抽出して集計しています。ファイル名のパターンをよく観察し、どの部分を抜き出したら「保存された日時」が取り出せるかを理解することが重要です。

"260312141530.jpg"[0, 8].to_i  # => 26031214

このように、文字列操作やメソッドチェーンを使ってデータを処理します。irbなどのツールを使って一つずつ動作を確認しながら理解を深めると良いでしょう。

自動起動の設定

ここまでの実装で、手動でプログラムを起動してシステムを動かすことができました。しかし実際の運用では、電源を入れたら自動的にシステムが起動する方が便利です。systemdというLinuxのサービス管理の仕組みを使って自動起動を設定します。

まず、人物検知プログラム用のサービスファイルを作成します。

sudo nano /etc/systemd/system/yolo-detection.service

以下の内容を記述します(ユーザ名がpiuserでない場合は適宜変更してください)。

[Unit]
Description=YOLO Person Detection Service
After=network.target

[Service]
Type=simple
User=piuser
WorkingDirectory=/home/piuser/yolo_detection
ExecStart=/home/piuser/venv/bin/python3 detect_person.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

次に、Webアプリ用のサービスファイルを作成します。

sudo nano /etc/systemd/system/yolo-webapp.service

以下の内容を記述します。 実践課題5でrbenvを導入していますので、それに合わせた実行コマンドになっています。ユーザ名やパスは環境に合わせて変更してください。

[Unit]
Description=YOLO Detection Web Application
After=network.target yolo-detection.service

[Service]
Type=simple
User=piuser
WorkingDirectory=/home/piuser/yolo_detection/webapp
ExecStart=rbenv exec ruby main.rb -o 0.0.0.0
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

サービスを有効化して起動します。

sudo systemctl daemon-reload
sudo systemctl enable yolo-detection.service
sudo systemctl enable yolo-webapp.service
sudo systemctl start yolo-detection.service
sudo systemctl start yolo-webapp.service

サービスの状態を確認します。

sudo systemctl status yolo-detection.service
sudo systemctl status yolo-webapp.service

動作していることを確認したら、Raspberry Piを再起動して自動起動が正しく動作するか確認してください。

sudo reboot

再起動後、ブラウザからWebアプリにアクセスして、システムが動作していることを確認します。

サービスのログを確認するには以下のコマンドを使用します。

sudo journalctl -u yolo-detection.service -f
sudo journalctl -u yolo-webapp.service -f

トラブルシューティング

カメラが認識されない

カメラが正しく接続されているか確認してください。

ls /dev/video*

何も表示されない場合は、カメラが正しく接続されていません。USBカメラの場合は接続を確認し、CSIカメラの場合はrpicam-helloで認識されているか確認してください(現在のRaspberry Pi OSでは、raspi-configでカメラを有効化する必要はありません)。

画像が保存されない

ディレクトリのパーミッション(権限)を確認してください。

ls -la ~/yolo_captures/

書き込み権限がない場合は以下のコマンドで修正します。

chmod 755 ~/yolo_captures
chmod 755 ~/yolo_captures/motions
chmod 755 ~/yolo_captures/snapshots

Webアプリでアクセスエラーが発生する

HTTPエラーが発生する場合は、main.rbのpublic_folderの設定を確認してください。パスが正しく展開されているか確認します。

# main.rbに以下を追加してデバッグ
puts "Public folder: #{settings.public_folder}"

処理が遅い

Raspberry Pi 4以前のモデルでは、YOLOの処理に時間がかかる場合があります。以下の対策を検討してください。

  1. 最も軽量なnサイズのモデル(yolo11n)を使う(yolo11sやyolo11mは精度が上がる代わりに処理が重くなります)
  2. 画像サイズを縮小する(detect_person.py内でresizeを使用)
  3. 処理間隔を長くする(waitKeyの値を大きくする)

応用と拡張

このシステムをベースに、さまざまな拡張が可能です。

検知条件のカスタマイズ

特定の条件でのみ保存するように変更できます。

# 複数人が検知されたときのみ保存
person_count = sum(1 for box in result.boxes if int(box.cls[0]) == 0)
if person_count >= 2:
    # 保存処理

通知機能の追加

人物検知時にメールやLINEで通知を送ることができます。例えば、smtplibを使ったメール送信は以下のようになります。

import smtplib
from email.mime.text import MIMEText

def send_notification(filename):
    msg = MIMEText(f"人物を検知しました: {filename}")
    msg['Subject'] = '人物検知アラート'
    msg['From'] = 'your@email.com'
    msg['To'] = 'recipient@email.com'
    # SMTPサーバーの設定が必要

画像への情報書き込み

検知した画像に日時や人数などの情報を書き込むこともできます。

from datetime import datetime

# 画像に情報を書き込み
font = cv2.FONT_HERSHEY_SIMPLEX
timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cv2.putText(annotated_image, timestamp_str, (10, 30), 
            font, 1, (255, 255, 255), 2, cv2.LINE_AA)
cv2.putText(annotated_image, f"Person: {person_count}", (10, 70),
            font, 1, (255, 255, 255), 2, cv2.LINE_AA)

ストレージ管理

長期間運用すると画像が蓄積してストレージを圧迫します。古い画像を自動削除するスクリプトを追加できます。

import os
import time

def cleanup_old_images(directory, days=7):
    now = time.time()
    cutoff = now - (days * 86400)
    
    for filename in os.listdir(directory):
        filepath = os.path.join(directory, filename)
        if os.path.isfile(filepath):
            if os.path.getmtime(filepath) < cutoff:
                os.remove(filepath)
                print(f"削除: {filename}")

この関数をdetect_person.pyのメインループ内で定期的に呼び出すことで、古い画像を自動削除できます。

自動リロード機能の追加

新しい画像が追加されたときに、ブラウザを手動でリロードせずに自動的に画面を更新する機能を追加できます。ポーリング方式を使い、定期的に最新画像をチェックして変化があれば画像部分のみをリロードします。

main.rbの最後に以下のAPIエンドポイントを追加します。

# main.rbの最後に追加
get '/api/latest' do
  content_type 
  motion_dir = File.join(settings.public_folder, 'motions')
  snapshot_dir = File.join(settings.public_folder, 'snapshots')
  
  motion_latest = Dir.exist?(motion_dir) ? Dir.children(motion_dir).select { |f| f.end_with?('.jpg') }.max : nil
  snapshot_latest = Dir.exist?(snapshot_dir) ? Dir.children(snapshot_dir).select { |f| f.end_with?('.jpg') }.max : nil
  
  { 
     {  motion_latest,  motion_latest ? Dir.children(motion_dir).count : 0 },
     {  snapshot_latest,  snapshot_latest ? Dir.children(snapshot_dir).count : 0 }
  }.to_json
end

次に、views/index.erbの</script>タグの直前(load("activities", "/activities");の後)に以下のJavaScriptコードを追加します。

// 以下を load("activities", "/activities"); の後、</script> の前に追加

// 自動リロード機能
let lastMotion = null;
let lastSnapshot = null;

function checkForUpdates() {
  fetch('/api/latest')
    .then(function(response) { return response.json(); })
    .then(function(data) {
      // 検知画像に新しいファイルがあればリロード
      if (lastMotion && data.motions.latest !== lastMotion) {
        load("motions", "/motions");
        load("activities", "/activities");
      }
      // スナップショットに新しいファイルがあればリロード
      if (lastSnapshot && data.snapshots.latest !== lastSnapshot) {
        load("snapshots", "/snapshots");
      }
      
      lastMotion = data.motions.latest;
      lastSnapshot = data.snapshots.latest;
    })
    .catch(function(error) {
      console.log('Update check failed:', error);
    });
}

// 5秒ごとにチェック
setInterval(checkForUpdates, 5000);

この実装により、新しい画像がmotionsまたはsnapshotsフォルダに追加されると、最大5秒以内に自動的に画面が更新されます。ページ全体ではなく画像部分のみが更新されるため、スクロール位置は維持されます。サーバ負荷も軽量なJSON応答のみで、実装もシンプルです。

まとめ

この章では、YOLOを使った人物検知システムとSinatraを使ったWebアプリケーションを組み合わせて、実用的な監視カメラシステムを構築しました。

この章で学んだ内容は以下のとおりです。

この知識を応用することで、さまざまな画像認識システムを構築できます。例えば、特定の物体の検出、顔認識、ポーズ推定など、実践課題6で学んだYOLOの機能を組み合わせることで、より高度なシステムを作ることができます。

HTML、CSS、Ruby、JavaScriptの詳細については、インターネット上に豊富なリファレンスやチュートリアルがあります。エラーメッセージに遭遇したら、まずはメッセージをよく読んで何が問題なのかを理解することが重要です。分からない場合は、エラーメッセージで検索すると解決方法が見つかることが多いです。

参考URL

付録 Zero Wでセンサのデータを集める

この付録は、以前の版で「実践課題2」として掲載していた課題です。GUIの入っていない「Raspberry Pi Zero W」と、すでに販売を終了しているセンサボード「Enviro pHAT」を前提にしています。手元に同じ機材がない場合も、crontabによる自動起動や、データを集めてグラフにする流れは、他のRaspberry Piやセンサでも参考になると思います。

ネットワークに接続する

Wi-Fiの接続先(SSIDとパスワード)と国の設定は、OSのインストール時にRaspberry Pi Imagerで行います。国の設定は「Japan」にしてください。Raspberry Piの発する電波が法律に合致しているかどうかは国によって異なりますので、ここが正しく設定されていないとWi-Fiが使えないことがあります。

うまくつながっているかどうか確認をするためのコマンドをいくつか紹介します。「ip addr」コマンドでは、現在の自分自身のIPアドレスを確認することができます(下記例で10.0.1.14の部分)。

$ ip addr
: 省略
2: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast
:
     inet 10.0.1.14/24 brd 10.0.1.255 scope global wlan0

pingコマンドでは、インターネット上の別のサーバと通信ができるかどうか、またその際の遅延(速度)を確認することができます。

$ ping lumber-mill.co.jp
PING lumber-mill.co.jp (133.167.86.214) 56(84) bytes of data.
64 bytes from os3-373-19710.vs.sakura.ne.jp (133.167.86.214): icmp_seq=1 ttl=54 time=59.9 ms

コマンドを終了するには「Ctrl+C」をタイプしてください。

もう少し専門的なコマンド、tracerouteでは、目的のホストに到達するまでにどんなルータを経由しているかを調べることができます。

$ traceroute 133.167.86.214
traceroute to 133.167.86.214 (133.167.86.214), 30 hops max, 60 byte packets
 1  10.0.1.1 (10.0.1.1)  9.808 ms  13.609 ms  13.503 ms
 2  softbank221110226074.bbtec.net (221.110.226.74)  13.345 ms  13.162 ms  37.785 ms
: (省略)

追加のインストール(下記コマンド)が必要ですが、whoisコマンドを使うとインターネット上のドメインを誰が所有しているのか、いつ登録されたのか、などを調べることができます。

sudo apt install whois

例えば、google.comを調べると以下のような応答が得られます。

$ whois google.com
   Domain Name: GOOGLE.COM
   Registry Domain ID: 2138514_DOMAIN_COM-VRSN
   Registrar WHOIS Server: whois.markmonitor.com
   Registrar URL: http://www.markmonitor.com
   Updated Date: 2019-09-09T15:39:04Z
   Creation Date: 1997-09-15T04:00:00Z
: (省略)

プログラムを自動起動する(crontab)

Linuxにはcrontabというプログラムを決まった時間に起動するための仕組みがあります。 以下のコマンドで起動されるエディタを使って、設定を行います。 初回起動時のみ利用するエディタを聞かれますので、好みのものを選択してください。 ここまで、このテキストを読み進めて来ている方は、nanoがおすすめです。

crontab -e

たとえば、毎日15時にhello.pyというPythonのプログラムを起動するには以下のように書きます。

0 15 * * * python3 hello.py

前半の5つの数字(またはアスタリスク)がそれぞれ「分 時 日 月 曜日」となっていて、「*」が指定された場合は、「すべて」という意味になります。以下のように書くと、毎月1日の14:30に起動する、という意味になります。また、「#」で始めた行はコメントとして扱われ処理内容に影響しません。メモを残す場合などに利用します。

30 14 1 * * python3 hello.py
# Run only on the first day of each month.

他にも特殊な構文があります。@rebootに続けて書かれたコマンドは、システムが起動した直後に1度だけ呼ばれます。電源を入れたらいつも動かしたいプログラムがある場合は、ここに書いておくと便利です。

@reboot python3 hello.py

実際には、ただ起動するだけだと何が起こっているのか分からなくなることが多いため、以下のようにリダイレクトとパイプを使って(詳細は割愛)、マシンの上に記録(ログ)が残るように運用すると便利です。

@reboot python3 hello.py 2>&1 | logger -p cron.info -t "hello"

ログはsystemdのジャーナル(journal)に記録されています。上記のように hello というタグをつけた場合、以下のように確認することができます。

sudo journalctl -t hello

現在の設定を確認するには、「-e」の代わりに「-l」オプションを使用します。

crontab -l

「-r」オプションを指定すると、設定がすべて消去されます!

crontab -r  # danger!!

センサの値を取得する(envirophat)

以下のスクリプトでは「Pimoroni envirophat」というRaspberry PiのGPIOに装着可能なボードを使って各種のセンサ値を取得、ファイルに保存しています。

#!/usr/bin/env python3

# Based on Pimoroni example all.py (MIT License).

import sys, time
from envirophat import light, weather, motion, analog

INTERVAL = 1 # sec

def write(line):
    print(line)
    fn = "envirophat-%s.csv" % (time.strftime("%y%m%d"))
    with open(fn, mode='a') as f:
        f.write(line+"\n")

try:
    while True:
        # Temp,Light,Accelerometer X,Y,Z
        vs = [round(weather.temperature(),2),light.light()]
        vs += [round(x,2) for x in motion.accelerometer()]
        write(",".join(map(str,vs)))

        time.sleep(INTERVAL)

except KeyboardInterrupt:
    pass

Enviro pHATは、2021年2月の時点で公式サイトでの販売を終了していました(在庫が手に入る国内の通販サイトはあるかもしれません)。代わりに、Enviro+というセンサが入手可能です(2026年10月時点で販売中)。こちらは、より多くのセンサの値を取得することができます。Pythonのライブラリを介して同じような手順でデータを集めることができます。詳細は「Raspberry Piを用いたIoT製品制作(2020年9月)」を参照してください。

以下の実習課題は、「Pythonの基本」で学んだ内容と組み合わせて取り組んでみてください。

データロガーを作る(端末編)

ここまでの課題で学んだ技術を組み合わせて、Raspberry Piをデータロガーに仕立てます。電源やネットワークの無い複数の箇所に設置して運用することを想定していくつかの機能改良をしましょう。

IoTと呼ばれる分野では、例えば広い農場や工場のあちこちにセンサを配置して、そのデータを一箇所に集め、いつでも見られるようにする仕組みがよく使われます。本書では、各端末のデータを自前のサーバ(次節の「サーバ編」)に転送して、集計とグラフ表示まで行います。なお、データの受け取りやグラフ表示をクラウド上で提供するサービス(国内のAmbientや、海外のThingSpeakなど)を利用する方法もあります。サーバを自分で用意したくない場合は、こうしたサービスも検討してみてください。

専用のディレクトリを用意して新しいスクリプトを書きます。

mkdir enviro-logger
cd enviro-logger
nano main.py

既に、前節の演習問題の回答があれば、それをコピーして再利用しましょう。ない場合は、envirophatのサンプルコードをベースにしてください。

cp ../(前節で作ったpyファイル) ./main.py

少しスクリプトのサイズが大きくなるので、上記のコピーコマンドを使って動いているところまでを保管しておくなど工夫をしながら進めると良いでしょう。以下の解説は演習問題への回答状況によっては、既に実装済みの場合もあるかもしれません。その場合は、適宜スキップして進めてください。

まずは、argparseというライブラリを導入して、スクリプトの動作を実行時に変更することができるようにします。importにargparseを追加します。

import argparse, os, sys, time

コマンドライン引数を解析するためのコードを追加します。最終行のprintは動作確認のために追加しています。確認ができたら消してください。

parser = argparse.ArgumentParser(description='Envirophat interface')
parser.add_argument('-g','--graph',action='store_true',help='Show graph')
parser.add_argument('-n','--name',help='Name of CSV')
parser.add_argument('-i','--interval',type=int,default=1)
args = parser.parse_args()
print(args) # for debug. remove it later.

これによって、一気にコマンドらしさが増すと思います。例えば以下のように打つと、使い方のヘルプが表示されます。

python3 main.py -h

以下のように引数を指定して、どんな値が出力されるか確認してみましょう。

python3 main.py -n myroom -i 60

ファイルにデータを保存する箇所は以下のようになります。ファイル名を前述のargparseで取得したnameを使って作成しています。このnameを各端末ごとに重複しないように設定することによって、複数のセンサからのデータを一箇所に集めても違う名前で共存できるようになります。

def write(line):
    fn = os.path.dirname(__file__)
    fn += "/%s-%s.csv" % (args.name,time.strftime("%y%m%d"))
    sys.stderr.write("File: %s" % (fn))
    with open(fn, mode='a') as f:
        f.write(line+"\n")

これらをまとめると以下のようになります。ちょっと長いですが、上記の説明を参考にしていただきつつ実装をしてみてください。

#!/usr/bin/env python

# Based on Pimoroni example all.py (MIT License).
# See also: https://lmlab.net/books/2102_raspi/index.html#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%AD%E3%82%AC%E3%83%BC%E4%BD%9C%E3%82%8B%E7%AB%AF%E6%9C%AB%E7%B7%A8

import argparse, os, sys, time
from envirophat import light, weather, motion, analog

def write(line):
    fn = os.path.dirname(__file__)
    fn += "/data/%s-%s.csv" % (args.name,time.strftime("%y%m%d"))
    sys.stderr.write("File: %s\n" % (fn))
    with open(fn, mode='a') as f:
        f.write(line+"\n")

def draw_graph(v,max_v):
    w = 60
    i = int(v * (w / max_v))
    sys.stderr.write("\033[92m")
    sys.stderr.write('\r' + ('o' * i) + ' ' * (60-i))
    sys.stderr.write("\033[0m")
    sys.stderr.write(' ' + ('%04d' % (v)))
    sys.stderr.flush()

parser = argparse.ArgumentParser(description='Envirophat interface')
parser.add_argument('-g','--graph',action='store_true',help='Show graph')
parser.add_argument('-n','--name',help='Name of CSV')
parser.add_argument('-i','--interval',type=int,default=1)
args = parser.parse_args()

try:
    while True:
        d = time.strftime("%F")
        t = time.strftime("%T")
        vs = [d,t]
        # Temp,Light,Accelerometer X,Y,Z
        temp = weather.temperature()
        ligh = light.light()
        vs += [round(temp,2),ligh]
        vs += [round(x,2) for x in motion.accelerometer()]
        if args.graph: draw_graph(ligh,5000)
        else: write(",".join(map(str,vs)))

        time.sleep(args.interval)

except KeyboardInterrupt:
    pass

このスクリプトの動作確認(上記の実行例参照)がとれたら、以下のコマンドでcron(自動実行の設定)を編集します。

crontab -e

最後尾に以下のように記述します。

@reboot python3 enviro-logger/main.py -n myroom -i 10

マシンを再起動します。

sudo reboot

enviro-loggerディレクトリに自動的にログが溜まっていることを確認してください。nanoエディタでも確認できますが、tailというコマンドを使うと、ファイルの最後尾だけを切り取って表示することができ、こうした確認作業には便利です。例えば、以下のように使用します(ファイル名は設定に合わせた値に読み替えてください)。

tail enviro-logger/myroom-210205.csv

うまくいかない場合は、以下のように出力をログに保存する設定を追加して原因究明の手がかりを探します(「プログラムを自動起動する(crontab)」参照)。

@reboot python3 enviro-logger/main.py -n myroom -i 10 2>&1 | logger -p cron.info -t "enviro"

loggerコマンドにリダイレクトされた出力は以下のコマンドで確認することができます。

sudo journalctl -t enviro

今回、i2cのインタフェースの初期化よりも先にプログラムが起動してしまい「センサが見つからない」というエラーになってしまっていたので、起動をわざと60秒遅くして対応しました。また、上記の設定は動作確認がしやすいように10秒単位でデータを取得していますが、いったん動作確認が済んだら、(長期間設置する場合などログが大きくなり過ぎそうな場合を想定して)適宜間隔を大きくして調整をしてください(-i 600など)。最終的に、以下のように設定して、正しくファイルが蓄積されるようになりました。

@reboot sleep 60; python3 enviro-logger/main.py -n myroom -i 600 2>&1 | logger -p cron.info -t "enviro"

次に、このデータを集信用のサーバに転送します。次節の「サーバ編」で構築したサーバ(enviro-host)が同一のネットワーク内で稼働していて、ログインパスワードが分かっている前提です。

まずは、他のサーバにアクセスするのに必要な鍵のセットを作成します。いくつか入力を促されますが、すべてそのままエンターを押して先に進んでください。

ssh-keygen

鍵は、.sshという隠しフォルダに作成されます。これをサーバに登録するには以下のコマンドを実行します。正常に接続できるとパスワードを聞かれますので、このサーバのパスワードを入力してください。

ssh-copy-id -i ~/.ssh/id_ed25519.pub enviro-host.local

ここでうまくつながらない場合、ネットワーク接続に問題がある場合があります。この付録の最初に紹介したip addrやpingなどのコマンドを使ってネットワークへの接続状態を確認してください。名前解決に問題がある場合、端末やサーバを再起動することで改善する場合があります。

鍵の登録ができたら、以下のコマンドで接続が可能かを確認します。サーバの名前(enviro-host)が表示されたら正常です。

ssh enviro-host.local hostname

データをサーバに転送するにはrsyncというコマンドを利用します。

rsync -avz enviro-logger/data/*.csv enviro-host.local:enviro-logger/data/

これもmain.pyと同じようにcronに登録してしまいましょう。 こちらは、main.pyのintervalとのバランスを見て間隔を決めてください。以下は10分おきに転送を起動する例です。

*/10 * * * * rsync -avz enviro-logger/data/*.csv enviro-host.local:enviro-logger/data/

データの蓄積ができたら、続いて、統計データを作成するためのスクリプトを用意します。こちらは、同じディレクトリの中にstat.pyという名前の別のスクリプトに記述します。

cd enviro-logger
nano stat.py

保存するファイル名はenviro-logger/data/(name)-stat.csvとします(日付の代わりにstatという固定の名前を入れる格好です)。以下のような流れで処理を実行します。

  1. コマンドライン引数(-n)の名前を使って、保存するファイル名を決める
  2. 既に保存されている日別のデータファイルを一つずつ読み込み
  3. さらにファイルごとに1行ずつ値を読み込み
  4. 最低気温、最高気温、平均気温、最低照度、最高照度、平均照度を求めます
  5. 結果をコンマ区切りの1行にまとめます
  6. これらを保存対象のファイル(name-stat.csv)に保存します

全体のスクリプトは以下のようになります。

#! /usr/bin/env python

import argparse, os, sys

def stat(fn):
    # 日単位の、気温、照度の最低、最高、平均
    # date,(temp)min,max,avg,(light)min,max,avg
    # ex. yyyy-mm-dd,3,17,10,100,1500,500
    results = ["",99.99,0,0,9999,0,0]
    n = 0
    with open(fn, mode='r') as f:
        for line in f:
            row = line.strip().split(",")
            results[0] = row[0] # date
            temp = float(row[2])
            light = int(row[3])
            if results[1] > temp:
                results[1] = temp
            if results[2] < temp:
                results[2] = temp
            results[3] += temp
            if results[4] > light:
                results[4] = light
            if results[5] < light:
                results[5] = light
            results[6] += light
            n += 1
    results[3] = round(results[3] / n,2)
    results[6] = round(results[6] / n,2)
    return ",".join(list(map(str,results)))

parser = argparse.ArgumentParser(description='Envirophat interface')
parser.add_argument('-n','--name',help='Name of CSV')
args = parser.parse_args()

dir = os.path.dirname(__file__) + "/data"
fn = "%s/%s-stat.csv" % (dir,args.name)

sys.stderr.write("File: %s\n" % (fn))
with open(fn, mode='w') as fw:
    for f in sorted(os.listdir(dir)):
        if not f.endswith(".csv"): continue
        if f.endswith("stat.csv"): continue
        sys.stderr.write("  %s\n" % (f))
        fw.write(stat(dir+"/"+f)+"\n")

こちらもmain.pyやrsyncと同じようにcronに登録します。一日に一回、午前8時に更新するように設定する場合は以下のような書き方になります。実際の利用方法に即して適宜タイミングは検討してください。

0    8 * * * python3 enviro-logger/stat.py -n myroom 2>&1 | logger -p cron.info -t "enviro"

データロガーを作る(サーバ編)

この節の内容は、(Zeroではなく)Raspberry Pi 4などのよりハイスペックなマシンにWindowManagerをインストールした状態がおすすめです。

Menu -> Preferences -> Control Centre を起動します。

Systemタブで、Passwordを任意のものに、Hostnameを「enviro-host」に設定します。次にInterfacesタブで、SSHをEnableに変更します。最後に右下の「OK」を押します。この際、再起動が必要になる場合があります。

Terminalを起動し、データ集信用のディレクトリを作成します。2行目のコマンドも併せて実行し、デスクトップにリンクを作っておくと便利かもしれません。

mkdir -p enviro-logger/data
cd Desktop && ln -s ../enviro-logger

ここに集まったデータ(csv)を適宜、Excelなどのツールで解析して必要な知見を取り出すことができます。以下では、軽量なウェブサーバを立ち上げて、収集されたデータをビジュアライズ(グラフ化)する方法を紹介します。こちらは実習向けでなく、データロガーの実習を行う講師(運営)側で一台準備ができたら十分です。Google Chartを利用しますので、インターネットにつながっている必要があります。

Webview by Sinatra

ファイル構造は以下のようになります。sinatra.rbがウェブサーバ本体、.erbが各ページのソースです。

enviro-logger/
  - sinatra.rb
  - views/
    - index.erb
    - files.erb

必要な環境をインストールします。

sudo apt install -y ruby
sudo gem install sinatra rackup puma

sinatra.rbは以下のようになります。メインのページではCSVファイルを読み込んでグラフを描画します。もう一つ”files”というページでは、データディレクトリに届いたファイルの一覧を表示します。

require 'sinatra'

set :public_folder, __dir__ + '/data'

get '/' do
  @names = []
  @dates = []
  Dir.entries(settings.public_folder).sort.each do |f|
    next unless f.end_with? ".csv"
    begin
      n, d = f.match(/([a-zA-Z0-9]+)-([0-9]+)\.csv/)[1,2]
      @names << n unless @names.include? n
      @dates << d unless @dates.include? d
    rescue
      puts "invalid name: #{f}"
    end
  end
  @name = params[:name] || @names.first
  @date = params[:date] || @dates.last
  @csv1 = settings.public_folder+"/#{@name}-#{@date}.csv"
  @csv2 = settings.public_folder+"/#{@name}-stat.csv"
  if File.file?(@csv1) && File.file?(@csv2)
    erb :index
  else
    'File not found<br>'+@csv1+"<br>"+@csv2
  end
end

get '/files' do
  erb :files
end

views/index.erb は以下のようになります。

<html>
<head>
  <title>Envirophat</title>
  <style>
    body{
      font-family: Tahoma, sans-serif;
    }
    div#header{
      display: flex;
    }
    h1{
      font-size: larger;
      margin: 0 20px 20px 0;
    }
    h2{
      clear: both;
    }
  </style>
  <!--Load the AJAX API-->
  <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
  <script type="text/javascript">

    // Load the Visualization API and the corechart package.
    google.charts.load('current', {'packages':['line', 'corechart']});

    // Set a callback to run when the Google Visualization API is loaded.
    google.charts.setOnLoadCallback(drawChart);

    // Callback that creates and populates a data table,
    // instantiates the pie chart, passes in the data and
    // draws it.
    function drawChart() {
      // [chart1]
      // 直近1日分のグラフを描画します
      var data1 = google.visualization.arrayToDataTable([
        ['Time', 'Temp', 'Light'],
        <% File.foreach(@csv1).each do |line| %>
          <% row = line.strip.split(",")
            y,m,d = row[0].split("-").map {|v| v.to_i}
            h,mi,s = row[1].split(":") %>
          [<%= "new Date(#{y},#{m-1},#{d},#{h},#{mi},#{s})" %>, <%= row[2] %>, <%= row[3] %>],
        <% end %>
      ]);

      // Set chart options
      var options1 = {
        series: {
          // Gives each series an axis name that matches the Y-axis below.
          0: {axis: 'Temp'},
          1: {axis: 'Light'}
        },
        axes: {
          // Adds labels to each axis; they don't have to match the axis names.
          y: {
            Temp: {label: 'Temps (Celsius)'},
            Light: {label: 'Light'}
          }
        },
       'width':800,
       'height':300};

      // Instantiate and draw our chart, passing in some options.
      var chart1 = new google.charts.Line(document.getElementById('chart1'));
      chart1.draw(data1, options1);

      // [chart2]
      // name-stat.csvから日次のグラフを描画します
      // 上記のcharts.Lineでは最大最小の範囲を表現する記法がなかったため、
      // 一つ古いバージョン(corechart)のグラフを利用して描画しています。
      var data2 = new google.visualization.DataTable();
            data2.addColumn('date', 'Time');
            data2.addColumn('number', 'Temp');
            data2.addColumn({id: "min", type:'number', role:'interval'});
            data2.addColumn({id: "max", type:'number', role:'interval'});
            data2.addColumn('number', 'Light');
      <% File.foreach(@csv2).each do |line| %>
        <% row = line.strip.split(",")
          y,m,d = row[0].split("-").map {|v| v.to_i}
         %>
                data2.addRow([<%= "new Date(#{y},#{m-1},#{d})" %>, <%= row[3] %>,<%= row[1] %>,<%= row[2] %>,<%= row[6] %>]);
      <% end %>

      // Set chart options
      var options2 = {
        series: {
          // Gives each series an axis name that matches the Y-axis below.
          0: {targetAxisIndex: 0},
          1: {targetAxisIndex: 1}
        },
        vAxes: {
          // Adds labels to each axis; they don't have to match the axis names.
          y: {
            Temp: {label: 'Temps (Celsius)'},
            Light: {label: 'Light'}
          }
        },
        intervals: { 'style':'area' },
       'width':800,
       'height':300};
      var chart2 = new google.visualization.LineChart(document.getElementById('chart2'));
      chart2.draw(data2, options2);
    }
  </script>
</head>
<body>
  <div id="header">
    <h1>Envirophat</h1>
    <form action="/" method="get">
      <select name="name">
        <% @names.each do |n| %>
          <option value="<%= n %>" <%= "selected" if n == params[:name] %>><%= n %></option>
        <% end %>
      </select>
      <select name="date">
        <% @dates.each do |d| %>
          <option value="<%= d %>" <%= "selected" if d == params[:date] %>><%= d %></option>
        <% end %>
      </select>
      <button>Update</button>
    </form>
  </div>
  <div id="chart1"></div>
  <div id="chart2"></div>
  <p><a href="files">CSV files</a></p>
</body>
</html>

views/files.erbは以下のようになります。

<html>
<head>
  <title>Envirophat</title>
  <style>
    body{
      font-family: Tahoma, sans-serif;
    }
  </style>
</head>
<body>
  <a href="/">Back to top</a>
  <ul>
  <% Dir.entries(settings.public_folder).sort.each do |f| %>
    <% next unless f.end_with? ".csv" %>
    <li><a href="<%= f %>"><%= f %></a></li>
  <% end %>
  </ul>
  <a href="/">Back to top</a>
</body>
</html>

ここではRubyという言語を使っていますので、起動コマンドは以下のようになります。

ruby enviro-logger/sinatra.rb

終了する場合は「Ctrl+C」を入力してください。上記はテストモードで起動する場合で、エラーメッセージが詳細に表示されるなど便利ではありますが、サーバを起動したマシン以外からは閲覧できないという制限があります。同じネットワーク内の他のホストからも見えるようにしたい場合、APP_ENV=productionという環境変数を追加することで起動モードの切り替えが可能です。

マシン起動時に自動起動したい場合は、以下のように書いておくことができます。

@reboot APP_ENV=production ruby enviro-logger/sinatra.rb 2>&1 | logger -p cron.info -t "enviro"

付録:エスケープシーケンス

033(16進数だとx1b)で始まる特殊記号の後ろに番号を付けることでターミナルへの出力を装飾することができます。グラフを描く例でも出たように、色を付ける場合は以下のように記述します。

import sys
sys.stderr.write("\033[90mHello!\033[0m\n") # gray
sys.stderr.write("\033[91mHello!\033[0m\n") # red
sys.stderr.write("\033[92mHello!\033[0m\n") # green
sys.stderr.write("\033[93mHello!\033[0m\n") # yellow
sys.stderr.write("\033[94mHello!\033[0m\n") # blue
sys.stderr.flush()

他にもさまざまな装飾が定義されていますので、以下のようなスクリプトを書いて実際の環境でどのように見えるか確認してから使うと良いでしょう。

import sys

for i in range(0,100):
  sys.stderr.write("\033[%dm%d Hello!\033[0m\n" % (i,i))
  sys.stderr.flush()

補遺

参考資料

以前の版で参照していた資料

以下は、以前の版の執筆時に参照していた資料です。記録として残していますが、リンク切れになっていたり、現在のRaspberry Pi OSでは使えない内容が含まれていたりする場合があります。

更新履歴

本書について

2015年に執筆した入門書をベースにして、さまざまな課外授業向けに内容を加筆し続けています。

「CC BY 4.0」に則った再利用・再頒布が可能です(ただし、後述の「図版とコードの出典」に挙げた第三者の素材は、それぞれのライセンスに従います)。本書に関するお問い合わせ、誤字・脱字等のご指摘は同社問い合わせフォームにお寄せいただければ幸いです。著者のSNS等に直接メッセージいただく形でも問題ありません。一部のコードはGitHubに公開してあります。公開できないものは一切なく、ただただ筆者の怠惰によるものですので、「ここの詳細が欲しい」といったご要望があればぜひお寄せください。

著者: ITO Yosei 伊藤陽生 伊藤製作所・株式会社ランバーミル代表。2003年頃からシステム開発に携わっています。Cから始まりJava、JavaScript、Objective-C、PHP、 Python、SQLなどを扱ってきました。現在は主にRubyを業務に用いています。Linuxサーバの構築だったり、ウェブサイトのデザインをしたり、これらの分野の研修講師として働いていたこともあります。技術の移り変わりの速いこの領域においては、それぞれの経験値そのものよりも、いかに新しい技術にうまくキャッチアップし、チームで共有し、身近な問題の解決に結びつけられるか、という、もう一段抽象的な技能こそが重要なのではないかと考えています。

図版とコードの出典

本書の文章・図版・コードは、以下を除き、著者が作成したものです。以下の素材は、それぞれのライセンスに従います。

謝辞とご協力のお願い

ここまで目を通してくださりありがとうございます。2015年ごろからRaspberry Pi関連のお仕事の機会をくださっているクライアント企業の方々のおかげで私自身が学びを深める機会を持つことができ、このような形でこれまでの経験とノウハウを蓄積することができました。

とはいえ執筆には少なくない時間が必要です。当初はこのテキストをepubやPDFに変換し、オンライン書店(AmazonやApple Books)にて販売していましたが、たびたび大幅な改訂が必要になり、そのメンテナンスが追いついていない現状があります。今後、こうした不整合を避けるためウェブに一本化していく予定です(コピーガードなしのepubも配布しています)。もしこの本の情報が何らかの役に立った、という部分があれば、以下のリンクから応援購入をいただければ幸いです。今後も継続してアップデートを続ける上で大変励みになります。

ご要望や誤字・誤りの指摘などは(こちらではなく)上記のお問い合わせフォームやSNS等から直接筆者にフィードバックをいただければ幸いです。

応援購入(Stripe)

payment link by Stripe