2026年1月、5回目の改訂に着手しました。RaspberryPiOSの最新バージョンはTrixieになりました。著者がRaspberryPiに触り始めてから10年以上が経過しましたが、その間、RaspberryPi自体の性能も大幅に向上し、OSも安定してきたことで、教育用のコンピュータとしてだけでなく、実際の業務用途にも耐えうるプラットフォームになってきたと感じています。本書では、RaspberryPiOS Trixieをベースに、画像解析やAIなどの最新の技術動向も踏まえた内容を紹介していきます。
2024年版までは古いOS向けの内容も併記するスタイルをとっていましたが、今回からは最新版の内容のみにして内容をスリム化しています。Bookworm以前の情報が必要な場合は、2024年版(Dive into Raspberry Pi 2024)をご参照ください。
当初のターゲットが「高校生向け」ということもあり、基本的な部分のみに着目し、なるべく専門用語を避ける、あるいは用語の解説をつけるように心がけています。それでも、例えばコントロールキーと普通のキーを組み合わせてコマンドを実行するなど、コンピュータを扱う上での基礎的なリテラシはある程度必要になってきます。また、インストールを始めとしてほとんど全ての操作は「英語」で行っています。解説通りに進めば英語の知識はそれほど必要ありませんが、本書の内容を応用したり、予期しないエラーに遭遇してしまう場合など、次のステップに進む際には、ある程度の「英語力」は必要になります。著者がそうしてきたように、好きなことをやりながら辞書を片手に(AIやオンライン辞書を横に置いて)一緒に英語も学んでいくスタイルが一石二鳥かと思います。
本書に登場する主な言葉を解説します。コンピュータの仕組みを解説すると、どうしてもカタカナ(英語)の用語が頻繁に登場することになりがちです。なるべく平易な表現を心がけていますが、実際の現場で使われる語彙とかけ離れてしまうのも問題です。幸い、現在では、Wikipediaなどのインターネット上のリソースも豊富です。意味の分からない単語に出会ったらそのままにせず、Googleなどの検索エンジンで一度調べてみることをお勧めします。著者の私は、この分野で10年以上働いていますが、未だに知らない(新しい)単語にしょっちゅう出会います。「知らない」のが普通です。「どうやって知るか」という方法を身につけることが大切です。コンピュータやインターネットの用語集を手元に持っておくのも良いかもしれません。
イギリスで教育用に開発された手のひらサイズの小さなコンピュータ。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」なども本書で扱ってきています。各モデルの大きな違いは処理性能ですが、備えている端子の種類がそれぞれ異なっていますので、必要な周辺機器が使えるかはモデルごとに確認が必要です。
Linuxディストリビューションの一つである「Debian」をRaspberry Pi用にカスタマイズしたディストリビューションです。OSとしての基本機能のみならず、35000以上のパッケージが用意されており、カメラモジュールのドライバやコマンドも最初から整っており、ScratchやSonicなどの教育用のアプリが充実している点も魅力です。本書では、RaspberryPiOSをインストールしたRaspberry Piをベースに課題に取り組みます。
専門的な用途に使う場合は、RaspberryPiOS以外にもUbuntu,PidoraやArchLinuxなど様々な選択肢が用意されています。
まつもとゆきひろ氏が開発したプログラミング言語です。ウェブサービスを立ち上げるための言語として有名ですが、その他にもたくさんの機能があって世界中で幅広く使われています。フリーソフトウェアとして配布されています(誰でもお金を払うことなく使えます)。執筆時点での最新バージョンは4.0ですが、RaspberryPiOSにはそれより少し古いバージョン3.1が提供されています。もちろん新しい方を使うほうがベターなんですが、基本的な用途では多少古いバージョンでも問題なく使えます。
本書の実習では、rbenvという拡張ツールを使って複数のバージョンを切り分けて利用する方法も紹介します。
オランダのGuido van Rossum氏が開発したプログラミング言語です。読みやすさを重視した言語構造が特徴で、Ruby同様、世界中で広く使われています。様々な専門分野のライブラリが充実していることも魅力です。現在のバージョン(3.x)では改善されているものの、過去の2.x系では日本語(マルチバイト文字)の扱いがRubyに比べると煩雑でした。筆者は元々、Pythonを業務に用いてきた経緯がありますが、現在はRubyをメインにしています。Rubyの方が、日本語テキストの処理は容易だったという(日本人ならではの)理由です。
RaspberryPiOSにデフォルトで搭載されたテキストエディタです。Linux(Unix)のテキストエディタといえば、viかemacsが有名ですが、nanoはこれらに比べてシンプルで覚えやすい操作性が特徴です(逆に、viやemacsは習得が困難な代わりに非常に高機能です)。本書ではnanoを使って、各種設定ファイルの編集や、プログラムの記述を行います。本書の後半で基本的な使い方を解説しています。
この章では、Raspberry Piを利用するのに必要な周辺機器とインストールから起動までの手順を解説します。Raspberry Piが出始めた頃は日本で入手するのは難しい時期もありましたが、現在はAmazonなどの大手ネットショップでも比較的簡単に見つかるようです。値段等はまちまちなので、複数比較して納得のいくものを選択すると良いでしょう。筆者はRaspberry Pi Shop by KSYや、SWITCH SCIENCEなどをよく使います。
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接続のテレビなどを利用して後はリモートアクセスで開発を進めるなど、状況に応じて工夫してください。
公式サイトで配布されているRaspberry Pi Imagerというアプリケーションを利用して、RaspberryPiOSがインストールされたSDカードを作成する方法をオススメします。Mac、Windows、Ubuntu版が用意されています。
以前はddコマンドを使ってSDカードにOSのイメージをコピーしていましたが、引数を間違えると事故につながるため、極力上記のインストーラを利用してください。
インストールする際に、ホスト名やユーザ名を任意に設定することが可能です。本書の解説では、ホスト名をraspi5、ユーザをpiuserと設定したイメージを利用しています。ご自身の設定内容に合わせて、適宜読み替えてください。
MicroSDカードをRaspberry Pi本体に挿入し、電源ケーブルを接続します。初回起動時には、画面の指示に従って、言語やタイムゾーン、キーボードの選択、WiFiの設定などを行います。
左上のメニューから Preferences -> Control Centre と進み Interfaceのタブを選択します。コマンドラインからは以下のコマンドで起動可能です(見た目は異なります)。
sudo raspi-config
必要なインタフェース(接続手段)をEnableに変更してください。これらはRaspberryPiを遠隔から扱う上で便利なものですが、一方でセキュリティ上のリスクを増大させるものでもあります。パスワードを強固なものにしたりするなどの対策も合わせて検討が必要です。
また、この他にRaspberry Pi Connectというツールが付属するようになりました。初回にアカウント作成が必要ですが、これを利用すると同じローカルネットワークからのみならず、インターネット側からでも気軽に接続ができるようになります。
Raspberry Piを複数台使用する場合、名前(普通にインストールするとraspberryという名前がついています)が重複するとうまく接続できなくなりますので、重複のない名前に変更する必要があります。同じ設定ツールの中の「System」タブにある「Hostname」という項目から変更してください。
古いバージョンのVNCではディスプレイが繋がってデスクトップマネージャが起動していないと利用できません。その場合、ディスプレイを繋がずにリモートからのみ利用する場合、/boot/config.txtの以下の設定を有効にします。
hdmi_force_hotplug=1
24.04時点の最新のRaspberryPiOSでは、仮想のディスプレイが稼働しているため。ディスプレイなしでも利用が可能ですが、アプリケーションによっては、規定のサイズでは領域が足りずに表示しきれない部分が出てくることがあります。その場合、以下のように解像度を指定することも可能なようですが、、筆者の環境ではうまくいきませんでした(立ち上がるけれども、接続しても正常に使えませんでした)。
vncserver-virtual -RandR=1920x1080
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:~ $
この表示をプロンプトと呼びます。RaspberryPiOSを始め、Linux系のOSでは、ここから様々な命令をコンピュータに対して送ることが出来ます。MacやWindowsにも似たような画面が存在します。MacのコマンドはLinuxととてもよく似ていますが、Windowsだけはかなり違った言語を使います。
$のマークの後ろにカーソルがあることを確認したら、以下の命令を打ってみましょう。文字を打ち終わったらEnterキーを押します。
ls
lsは今いる場所のファイル及びディレクトリを一覧表示してくれるコマンドです。以下のように
(ファイルやディレクトリの)名前がいくつか表示され、また元のプロンプトが表示されていることを確認してください。
piuser@raspi5:~ $ ls
Documents Pictures Public..
(略)
piuser@raspi5:~ $
このターミナルを主に使って、様々な設定やプログラムを書いていきます。
本書では設定ファイルやプログラムの編集に「nano」エディタを使用しています。ここでは基本的な使用方法を紹介します。さらに詳しい使い方はnanoのヘルプページ(起動後に「^G」で表示)や公式サイトを参照してください。
起動は「nano」コマンドに続けて編集したいファイル名を指定します。新規作成の場合も同様です(保存時に指定した名前のファイルが生成されます)。
nano sample.txt
基本的な操作は、おそらくWindowsやMacを使ったことがあれば問題なく行えるのではないかと思います。カーソルキーで移動して、文字をタイプ、Backspaceキーで削除したりReturnキーで改行、などは一般的なテキストエディタと同じです。画面の下部に主要な操作が列挙してあります。白地に黒抜きで表示してある部分が、その操作を実行するためのキーです。「^G」は「Ctrl」キーを押しながら「G」キーを押す、という意味になります。
以下の操作を覚えていれば本書で扱うくらいのプログラムの記述には十分です。
「^O」 - ファイルを保存します。
「^K」 - カーソルがある行をカットします。
「^U」 - カットした行をペーストします。
「^X」 -
終了します。未保存のファイルがある場合は、保存するか確認されます。
シンタックスハイライトや検索の機能もあって、初心者でもそこそこ使える便利なエディタという感じですが、本格的にプログラミングを学びたいという場合には、IDE(例えばNetbeansやEclipse)やVSCodeなどの後進のエディタ利用も選択肢に入れても良いかもしれません。Raspberry Pi OSには、ThonnyとGeanyという簡易的なIDEも標準でインストールされています。VimやEmacsといった歴史のあるエディタも使いこなせば強力です。
GPIOやカメラなどRaspberry Piから外部のデバイスを操作するライブラリの多くはPythonという言語で提供されています。他の言語でも扱えないことはないのですが、インターネット上に情報が多く、安心して利用できるメリットがあります。そもそもPython自体が、とても覚えやすいシンプルな言語ですので、RaspberryPiを使いこなそう!と思ったら必ずと言って良いほど触れる機会が出てくる思います。ここでは、基本的な構文について幾つか紹介します。
まずは、Pythonを起動する方法と、画面に文字列「Hello, world!」を表示する方法です。
$ python
Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("Hello world!")
Hello world!
PythonはMacOSにも最初から入っているくらいメジャーな言語です。もちろん、RaspberryPiOS上でも、同じようにPythonを起動することができます。以降のサンプルはどちらでも同じように動作します。
「>>>(プロンプトと呼びます)」の記号の後ろに、Pythonに対する命令を入力して実行することができます。まず、以下のように数式を打って見ましょう。プロンプトの後ろを実際にタイプします。その次の行に表示されているのが実行結果です。
>>> 1 + 3
4
>>> 5 * 20 / 4
25
ちょっとした電卓がわりにも使うことが出来そうですね。「*(アスタリスク)」は掛け算、「/(スラッシュ)」は割り算の記号として使われます。
Pythonを終了するときは「Ctrl+D(Ctrlキーを押しながらD)」を入力します。
ちょっとした命令なら、上記のように起動、入力(実行)という手作業でも問題ありません、少し複雑なことをしたい場合は、プログラム(命令)を予めファイルに書いておいて、それを実行するという方法を取ることになります。Pythonのプログラムが書かれたファイル(スクリプト、などと呼びます)の名前を現在のディレクトリに置いて、以下のように指定します。
python foo.py
スクリプトの作成と保存は、前述のnanoエディタなどを使うと便利です。RaspberryPiOSで利用できるエディタは他にも色々ありますので、是非調べてみてください。
以降の実例に出てくるように、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.
モジュールのインポートは本書でも何度か登場します。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:purple
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 demo
optional arguments:
-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:purple
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)
RaspberryPiで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]]
日付処理とコマンドライン引数の利用例として、指定した年月のカレンダーを表示するプログラムを書いてみましょう。
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"
Gitは現在、おそらく世界で最もメジャーなバージョン管理システムです。github、gitlab、bitbucketなど様々なソースコードのホスティングサービスが存在しますが、それらの共通の基盤になっています。
Raspberry Piでもgitを使うことができます。まずは以下の手順で、手元に一つリポジトリを作ってみましょう。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 show ...
git checkout ...
git switch ...
このようにソースコードの変更を記録し、いつでも参照したり巻き戻したりできることは、ある程度以上の規模のプロジェクトでは必須とも言える要件になってきます。個人のプロジェクトでも長期に渡ってメンテナンスする場合にはとても有用です。
参考: https://git-scm.com/
RaspberryPiで出来ることを幾つか紹介していきます。デスクトップやターミナルの基本的な操作も合わせて学びながら、RaspberryPiを使いこなすための第一歩を踏み出してみましょう。
カメラ専用の接続端子に接続するカメラモジュールが必要です。接続済みの状態で、以下のコマンドが利用できるか確認してみましょう。
rpicam-hello
静止画を撮るには、rpicam-still
コマンドを利用します。「-o」というオプションに続けて出力ファイル名(ここではpic2.jpg)を指定しています。
rpicam-still -o pic2.jpg
コマンドの後ろに続く「ハイフン+アルファベット」の組み合わせはオプション、またはスイッチと呼ばれるもので、「-o」の他にも様々なものが用意されています。例えば「-t」は起動してから撮影するまでの待ち時間をミリ秒で指定できます(指定しない場合は5秒)。「-vf」「-hf」はそれぞれ、垂直反転、水平反転のオプションです。カメラの設置向きによって画像が反転してしまう場合がありますが、そういった際に向きを調節するのに便利な機能です。
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接続のカメラも利用可能です(配線や設置はこちらの方が簡単かもしれません)。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()
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!というメッセージを追加して変更が反映されることを確認しています)
このまま立ち上げておけば、社内や家庭内向けのウェブページや掲示板などの仕組みを運用することが出来ますね。世界に向けて公開する場合は、グローバル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://raspberrypi.local/pictures/
上記のようにファイルの一覧が表示されていたら成功です。画像名をクリックすると、撮影した画像を確認できます。ウェブアプリケーションを作って、スライドショーで見せたりサムネイルで一覧表示したりと使いやすくするアイデアは沢山出てきそうです。また、今回は直接コマンドを実行しましたが、この実行のタイミングを一定時間おき、あるいは決まった時刻に、など設定することも簡単です。後述する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>「Scratch」は教育用のプログラミング環境です。ブラウザから以下のアドレスにアクセスしてそのまま利用することが可能です。通常の利用はこちらで十分です。WindowsやMac環境からも全く同じように利用できます。
https://scratch.mit.edu/
プログラミングといっても、テキストエディタを使ってコードを書くものとは異なり、様々な機能を持ったブロックをマウスで操作して繋げてプログラムを作成します。また、絵を描くためのツールがあったり、音声(音楽)ファイルを追加して効果音を付けたり、プログラミングに必要な素材も様々なものが提供されています。簡単なゲームなどを作ることが可能です。公式サイトには世界中のユーザが作った作品が多数公開されていますので、ぜひ一度、覗いてみてください。
ローカル環境にインストールすることも可能です。こちらからインストールするとRaspberry Pi用の拡張機能を利用してGPIOにアクセスすることができます。
sudo apt install scratch3
※VNC経由で使っていたりモバイル用途の小さなディスプレイでは解像度が足りず、一部のボタンが隠れてしまうことがあります。利用する場合は、ある程度大きなディスプレイ環境を用意してください。
「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スピーカーへ出力先を切り替えることができます。
以下は公式サイトに掲載されていたサンプルプログラムです。このコードを「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:ドメイン固有言語」というキーワードで調べてみてください)。
23年12月時点、最新のOS(bookworm)では、以下のコマンドが利用できます。または
PrtScrキーを押すだけでもスクリーンショットを撮ることができます(DesktopではなくPicturesフォルダに保存されます)。
grim
3秒後にカーソル位置を含めたスクリーンショットを撮る場合は以下のようにします。
sleep 3 && grim -c
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に近い遊び方ができます。
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」を追加します。
RaspberryPiにスピーカを接続して日本語を喋らせることも出来ます(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 コマンド実行時に指定してください。
https://sourceforge.net/projects/mmdagent/files/MMDAgent_Example/MMDAgent_Example-1.7/
bookworm(最新のRaspberryPiOS)では、labwcという仕組みを使います(LXDE版は前著などを参照してください)。
デジタルサイネージというのは、駅や店頭に設置されたディスプレイに、訪れる人に有益な情報(運行情報、営業時間)や広告などを表示する端末のことです。Raspberry Piを普通に起動するとWindowsやMacのようなデスクトップ画面が表示されますが、この設定を変更してウェブブラウザだけを固定して表示することで実現できます。今回使用するChroniumブラウザでは「キオスク」モードと呼ばれています(デジタルサイネージと似ていますが、ユーザの操作を受け付けたり、もう少しインタラクティブ性を持ったものの呼称に使われることが多いようです)。
.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
Dive into RaspberryPi 2026
</div>
</div>
</body>
</html>
上記のファイルをホームディレクトリにsignage.htmlとして保存した場合、autostartに記述するコマンドは以下のようになります。--password-store=basic
は、Chromiumがキオスクモードで起動した際に、「Unlock
Keyring」のダイアログを表示しないようにするためのオプションです。また、wlrctlというツールを使って、10秒後にマウスカーソルを少しだけ動かすコマンドも追加しています(マウスが動かないとカーソルを非表示にするスタイルが効かないため)。
(sleep 10 && wlrctl pointer move +1) &
chromium --kiosk --incognito --password-store=basic signage.html
Ctrl + W で、Chromiumを終了させることができます。再度起動するには、OSを再起動するか、ターミナルから上記のコマンドを直接実行してください。
RaspberryPiも5になって、いよいよミニデスクトップPCとしての利用価値も高まって来ているように見えます。定番のお絵描きツールであるGIMPとInkscapeを試してみましょう。まずはインストールします。
sudo apt install gimp
インストールが済むとメニューに項目が追加されているはずですので、そこから起動します。
GIMPは、ビットマップ(ピクセル)画像を編集するための高機能な画像編集ソフトウェアです。写真の加工やレタッチ、デザインの作成など、Adobe Photoshopに匹敵する機能を持つフリーソフトウェアとして世界中で利用されています。
主な用途: - 写真の補正やレタッチ(明るさ、コントラスト、色調の調整) - スクリーンショットやWebグラフィックスの編集 - レイヤーやマスクを用いた複雑な画像合成 - フィルタエフェクトを使った画像加工 - テキストの追加や装飾
RaspberryPi 5では、ちょっとした画像編集や文字入れなどの作業は、ほぼストレスなく行うことができます。ただし、非常に大きな画像ファイルを扱ったり、複雑なフィルタ処理を行う場合は、処理に時間がかかることがあります。
実践例:印影を透過PNGに変換する
GIMPの便利な使い方の例として、判子の画像から印影だけを取り出して透過PNGにする方法を紹介します。これができるようになると、PDFに捺印したような画像を作成したり、様々な場面で応用できます。判子の写真から印影部分だけを抽出して、背景が透明な印影画像に加工する手順を見ていきましょう。念のため書いておくと、この方法はあくまで画像編集のテクニックの一例であり、実際の捺印の法的効力やセキュリティについては別途考慮が必要です。
手順:
gimp 画像ファイル名
でも起動できます。下の画像は、判子の写真をGIMPで開いた状態です。
最後に、できあがったファイルを書き出します。「File」→「Export
As…」を選択し、ファイル名を指定します。拡張子を.pngにすることで、PNG形式で保存されます。保存時のダイアログで「Save
background
color」のチェックを外し、透過情報が保持されるよう設定してください。
これで透過PNGの印影画像ができあがりです。この画像をPDFやWord文書の上に配置すれば、捺印したような見た目を作ることができます。
このように、GIMPには色で選択する機能や透過画像を作成する機能など、実用的な機能が多数備わっています。慣れてくると、写真の背景を削除したり、複数の写真を合成したりといった高度な編集も可能になります。
Inkscapeは、ベクター(パス)画像を作成・編集するためのグラフィックソフトウェアです。SVG(Scalable Vector Graphics)形式を標準フォーマットとして使用しており、拡大縮小しても画質が劣化しないという特徴があります。Adobe Illustratorの代替ソフトウェアとして広く知られています。インストールは以下のコマンドで行います。
sudo apt install inkscape
主な用途: - ロゴやアイコンのデザイン - ポスターやチラシなどの印刷物のデザイン - イラストや図形の作成 - 技術図面や回路図の作成 - Webサイト用のグラフィックス作成
ちょっとした文字や図形を描く程度であれば、ほとんど問題なく滑らかに動きます。本書で使われている図の多くも、Inkscapeで作成されています。
GIMPとInkscapeの使い分け
詳しい使い方に興味のある方は、書籍やウェブを探してみてください。AbobeのIllustratorやPhotoshopほどではありませんが、日本語の情報も含めてそこそこの資料が見つかると思います。また、公式サイトにはチュートリアルやドキュメントも豊富に用意されています。
sambaというサービスを使うことで、Finder(Windowsの場合エクスプローラー)からファイルを操作できるようになります。
まずはパッケージをインストールします。
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
実際に運用する場合には、バックアップやセキュリティの問題を考慮する必要がありますので、導入の検討は慎重に行なってください。
ここまでの課題では、カメラ・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プログラムからも呼び出せます。
pip install ollama
以下は、画像を入力として説明文を生成するサンプルです。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を使う、あるいはより軽量なモデルに切り替える、といった使い分けを推奨します。
Raspberry PiでLLMを使えるようになると、これまでの課題(カメラ、音声、Web)と組み合わせた応用が一気に広がります。例えば、カメラ画像を説明してWebに掲示する、センサ値を要約して音声で読み上げる、といった仕組みも作れるようになります。
この章は、GUIの入っていない「Raspberry Pi Zero W」向けの課題になります。
RaspberryOSではwpa_supplicantというデーモンでWi-fiに接続することができます。このデーモンの設定ファイルを直接編集する方法もありますが、ここではraspi-configを利用する方法を紹介します。まず、ターミナルで以下のようにコマンドを実行します。
sudo raspi-config
以下のようなメニュー画面が立ち上がります。
メニューを「2 Network Options -> N2 Wi-fi」の順に遷移し、Wi-fiのSSIDとパスワードを設定します。
続けて「4 Localisation Options -> I4 Change Wi-fi Country」と辿り、国を「Japan」に設定します。RaspberryPiの発する電波が法律に合致しているかどうかは国によって異なりますので、ここが正しく設定されていないと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
: (省略)
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"
ログは /var/log/syslog
というファイルに書き出されています。上記のように hello
というタグをつけた場合、以下のように確認することが出来ます。
sudo grep hello /var/log/syslog
現在の設定を確認するには、「-e」の代わりに「-l」オプションを使用します。
crontab -l
「-r」オプションを指定すると、設定が全て消去されます!
crontab -r # danger!!
以下のスクリプトでは「Pimoroni envirophat」というRaspberryPiのGPIOに装着可能なボードを使って各種のセンサー値を取得、ファイルに保存しています。
#!/usr/bin/env python3
# Based on Pimoroni exsamble all.py.
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
2021年2月現在、Enviro pHatは、公式サイトでは既に販売を終了しているようです(在庫が手に入る日本国内の通販サイトはあるかもしれません)。代わりに、Enviro+というセンサーが入手可能です。こちらは、より多くのセンサーの値を取得することができます。Pythonのライブラリを介して同じような手順でデータを集めることができます。詳細は「RaspberryPiを用いたIoT製品制作(2020年9月)」を参照してください。
以下の実習課題は、次章「Pythonの基本」で学ぶ内容と組み合わせて取り組んでみてください。
制御文字を使って、envirophatの値をグラフに表示するように修正してみましょう。
コマンドライン引数を使って、通常のファイル保存モードとグラフ表示モードを切替られるようにしてみましょう。たとえば、python3 enviro.py --graphと実行した場合のみ、グラフが表示され、それ以外ではファイルにデータを保存する動作をする、などです。
ここでは、インターネットを介してセンサーのデータを収集するための方法を試してみます。IoTと呼ばれる分野では、このアプローチが取られることが多いと思います。例えば、広い農場や工場のあちこちにセンサーを配置して一括で管理する場合、それぞれの端末まで直接データを集めに行くのは効率的ではありません。自動的に一箇所に集めて、いつでも見られるようにする仕組みが必要になります。
これを実現するための仕組みは沢山ありますが、今回は筆者が自分でクラウド上に立ち上げているサービスを利用します(こちらは試験運用中なので、実務に使う場合は、信頼できるサービスを選んでください)。
https://iot.lmlab.net/
上記のサービスにユーザ登録すると、デバイス(センサー)を登録することが出来るようになります。デバイスの登録が済むと、設定画面に以下のようなコマンド例が登場します。ここで利用しているのはcurlというコマンドです。このコマンドを使うとURL(インターネット上の住所
のようなもの)を指定して、データのやり取りができます。
curl -G -d "id=1" -d "token=secret" --data-urlencode "dt=2021-03-03T09:15:35+09:00" -d "temperature=12.3" -d "pressure=123.4" -d "humidity=12.3" -d "illuminance=123.4" -d "voltage=1.23" "https://iot.lmlab.net:443/temps/upload"
上記の例では、IDやトークン(合言葉)がダミーの状態ですので、実際にご自身で登録したデバイスの画面に表示されているコマンド例で送信を試してみてください。
前述したenvirophatなどはPythonのライブラリが提供されていますので、Pythonでつなぐと便利です。以下はPythonからcurlを利用する場合のサンプルです。
import datetime, os, time
from envirophat import weather, light
def get_url_params():
t = weather.temperature()
p = weather.pressure(unit='hPa')
h = None
l = light.light()
# Python's datetmie doesn't have timezone info.
# You may need to set system timezone as JST. (hint: sudo raspi-config)
ts = time.strftime("%Y-%m-%dT%H:%M:%S%z")
s = "dt=%s" % (ts)
s += "&temperature=%f" % (t)
s += "&pressure=%f" % (p)
# s += "&humidity=%f" % (h)
s += "&illuminance=%f" % (l)
return s
if __name__ == '__main__':
try:
url = os.environ["MUKOYAMA_URL"]
id = os.environ["MUKOYAMA_ID"]
token = os.environ["MUKOYAMA_TOKEN"]
u = (url+"/temps/upload?id=%s&token=%s&"+get_url_params()) % (id,token)
cmd = 'curl -s -S "'+u+'"'
os.system(cmd)
except KeyboardInterrupt:
pass
サーバのURLやID,TOKENなど、実行する端末ごとに異なる情報は環境変数という仕組みを利用してプログラムの外に記述しています。
実行する場合には、まず、以下のようにそれぞれのデバイスに合わせた値を環境変数にセットします(exportがそれをするためのコマンドです)。
export MUKOYAMA_URL=https://iot.lmlab.net
export MUKOYAMA_ID=1
export MUKOYAMA_TOKEN=secret
その状態でプログラムを起動すると、os.environという変数を通して、ここで設定された値が読み込まれ、それぞれの環境に応じた動作をします。
python3 iot.py
前述したcrontabでも、同じ仕組みが利用できます。ここに書く際にはexportは不要なので注意してください。
MUKOYAMA_URL=https://iot.lmlab.net
MUKOYAMA_ID=1
MUKOYAMA_TOKEN=secret
@reboot python3 iot.py 2>&1 | logger -p cron.info -t "iot"
ここまでの課題で学んだ技術を組み合わせて、RaspberryPiをデータロガーに仕立てます。電源やネットワークの無い複数の箇所に設置して運用することを想定して幾つかの機能改良をしましょう。
専用のディレクトリを用意して新しいスクリプトを書きます。
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 exsample all.py.
# 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 grep enviro /var/log/syslog
今回、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_rsa.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という固定の名前を入れる格好です)。以下のような流れで処理を実行します。
全体のスクリプトは以下のようになります。
#! /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='Envorophat 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ではなく)RaspberryPi4などのよりハイスペックなマシンにWindowManager(GUI)をインストールした状態がオススメです。
Menu -> Preferences -> Raspberry Pi Configuration を起動します。
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を利用しますので、インターネットに繋がっている必要があります。
ファイル構造は以下のようになります。sinatra.rbがウェブサーバ本体、.erbが各ページのソースです。
enviro-logger/
- sinatra.rb
- views/
- index.erb
- files.erb
必要な環境をインストールします。
sudo apt install -y ruby
sudo gem install sinatra
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>Envrophat</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>Envrophat</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の魅力の一つはGPIOを初めから備えていて、各種電子部品を簡単に接続することができる点です。LEDを光らせたり音を出したり、あるいはモータを駆動させて動き回ったり、逆に、光や音を検知してなんらかのアクションに結びつけたり、コンピュータが現実の世界とのやり取りを出来るようになると活用の幅がぐっと広がります。
GPIOとは、「General Purpose Input Output」の頭文字をとったもので、日本語にすると汎用の入出力、といった意味になります。Raspberry Pi以外にも多くのマイコンボード(例えばArduinoなど)でも使われている仕組みです。本体の端に沢山のピンが立っている部分がRaspberry PiのGPIOです。次節からGPIOの基本的な使い方をPythonのプログラムを使って確認してゆきます。以下の部品があれば、実際に動かしてみることも可能です。
公式にはPythonを使った説明が多く、恐らく(Pythonの環境が)最も整っていると思われますが、先述のRubyなど別の言語からでも扱うことができます。また、さらに専門的な回路(例えば発振を伴うようなもの)の制御にはC言語が必要になることもあります。
公式サイトに掲載されているピンの配置図を転載します。
さらに細かい役割については、公式サイトの図を参照してください。本書では、電源、接地の位置とGPIOの番号(黄色丸の中の数字)だけを主に使います。12,13はPWM用、というのも覚えておいても良いかもしれません。
2021年版まではRPi.GPIOというライブラリを使っていましたが、こちらがRaspberry
5では使えなくなってしまいました。pigpioというライブラリも5に対応しておらず、(githubのやりとりを読む限り)対応の目処もたっていなさそうです。ということで、gpiozeroというライブラリを使って作例を書き直していますが、RPi.GPIOも旧型のRaspberryPiでは使えるため、引き続き掲載しています。
まずは、LEDを点滅させる簡単な回路を組んでみましょう。以下ような回路を組み立ててください。黄色の線はGPIOの4番に、黒の線はGROUND(接地)につながっています。抵抗はどちら向きでも構いませんが、LEDは図に書いた向きでないと通電しませんので注意してください。この回路図は「Fritzing」というツールを使って描いたものです。
この回路を動かすPythonプログラムは以下のようになります。nanoエディタなどを使ってファイルを作成・保存してください。ファイル名はgpio-out.pyです。
import gpiozero, time
led = gpiozero.LED(4)
for i in range(5):
led.on()
time.sleep(1)
led.off()
time.sleep(1)
旧版のコード例:
import time
import RPi.GPIO as GPIO
sensor = 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(sensor,GPIO.OUT)
for i in range(5):
GPIO.output(sensor,GPIO.HIGH)
time.sleep(1)
GPIO.output(sensor,GPIO.LOW)
time.sleep(1)
GPIO.cleanup()
以下のコマンドで実行します。
python gpio-out.py
1秒間隔で5回、LEDが点灯したでしょうか?
旧版:RPi.GPIOの操作は管理ユーザ(root)の権限で行う必要があるため、先頭に「sudo」をつけてください。
次は、逆にGPIOのピンに対して入力があった(=電圧がかかった)状態の検出をしてみます。以下のような回路を組んでください。抵抗は先ほどのものと同じで、LEDの代わりにスイッチが配置され、GROUNDへの線(黒色)が無くなり、代わりに赤いジャンパ線が3.3Vの電源ピンに接続されています。ここでは説明のためジャンパ線の色分けをしていますが、もちろん、どの色を使って接続しても構いません。
プログラムは少し複雑になって、以下のようになります。ファイル名はgpio-in.pyです。
import gpiozero, signal
def print_high():
print("Pressed")
def print_low():
print("Released")
button = gpiozero.Button(4)
button.when_pressed = print_high
button.when_released = print_low
signal.pause()
旧版のコード例:
import time
import RPi.GPIO as GPIO
sensor = 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(sensor,GPIO.IN,GPIO.PUD_DOWN)
previous_state = False
current_state = False
while True:
time.sleep(0.1)
previous_state = current_state
current_state = GPIO.input(sensor)
if current_state != previous_state:
if current_state:
print "HIGH"
else:
print "LOW"
0.1秒ごとに、4番ピンの状態を取得し、前回取得した値と異なる結果が得られたら、スイッチが押された(または離された)と判断し、現在の状態を画面に表示します。このプログラムは延々と監視を続けますので、終了したい場合は、「Ctrl」キーを押しながら「C」をタイプしてください。
以下は、ボタンが押された際にSlackというチャットのプラットフォームにメッセージを送信する関数例です。AmazonがDashボタンという新製品を出した時に、面白そうだと思って真似て作ったのですが、その後、Dashボタンもサービスを終了するに至って、こちらもお蔵入りしてしまっていました。
def slack():
import requests, json
WEB_HOOK_URL = "https://..." # Slack の URL
requests.post(WEB_HOOK_URL, data = json.dumps({
'text': 'Notification From Python.', #
'username': 'Python-Bot',
'icon_emoji': ':smile_cat:',
'link_names': 1,
}))
gpiozeroから利用する場合は、ボタン押下時に呼ばれる関数としてセットしてあげればOKです。
button.when_pressed = slack
※本節は、公式サイトで紹介されている「Parent Detector」の和訳(一部要約)になります。
必要なもの Raspberry Pi本体の他に以下のパーツが必要です。
PIRモーションセンサは、赤外線人感センサなどと呼ばれることがあります。赤外線を使って、人や動物の動きを検知します。街灯や玄関などの照明で、人が通ると勝手に点灯するタイプのものにはこのセンサが使われています。まずは、このセンサをRaspberry PiのGPIOに接続しましょう。画像は公式サイトから引用させて貰っていますが、この図の結線と実際のセンサ上の端子の順番が異なっているので注意してください(細かい型番ごとなどに複数のバリエーションがあるのだと思います)。 「GPIOの5VとPIRセンサのVCC」「GPIOのGROUNDとPIRセンサのGND」「GPIOの4番ピンとPIRセンサのOUT」端子をそれぞれ結線してください。
まずは、この回路が正しく動くか検証するプログラムを書きましょう。nanoエディタで以下のコードを記述して保存してください。ファイル名はgpio-pir.pyです。
import gpiozero, signal
def print_high():
print("motion detected!")
ms = gpiozero.MotionSensor(4)
ms.when_motion = print_high
signal.pause()
旧版のコード例:
import RPi.GPIO as GPIO
import time
sensor = 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(sensor, GPIO.IN, GPIO.PUD_DOWN)
previous_state = False
current_state = False
while True:
time.sleep(0.1)
previous_state = current_state
current_state = GPIO.input(sensor)
if current_state != previous_state:
new_state = "HIGH" if current_state else "LOW"
print("GPIO pin %s is %s" % (sensor, new_state))
以下のコマンドで実行します。
python gpio-pir.py
センサの前で(といってもかなり広範な範囲を拾ってしまうのですが)、体を動かすと以下のような表示が変化したでしょうか。うまく反応しない場合は、センサの設定を調整する必要があります(ここが、このプロジェクトの肝だったりします)。
motion detected!
motion detected!
センサの下部に二つ並んだ黄色いツマミで、センサの感度と反応時間を調整します。プログラムを動かしたまま、サイズの合ったプラスドライバでツマミを動かし、思い通りの反応をする位置を探してください。ほんのちょっと動かしただけでも、結果が大きく変わることもありますので、ここは根気よく試行が必要です。
調整が終わったら、「Ctrl + C」でプログラムを終了してください。
次はいよいよ、Parent Detector本体のプログラムです。以下には完成版をそのまま掲載しました。実際には、先ほどのgpio-pir.pyをコピーして、カメラのプレビュー部分などから少しずつ書き足しながら都度、動作確認して記述してゆく書き方が良いかと思います。プログラミング言語に慣れていない場合、あまり一気に書き上げると、エラーが起こった時に原因箇所を突き止められなくなってしまうことがあります。
以下(新版)のコードは、センサに反応があると写真を保存します。
import cv2, gpiozero, signal, time
def take_picture():
global cam
fname = "%d.jpg" % (time.time())
print(fname)
ret, image = cam.read()
image = cv2.resize(image,None,fx=0.5,fy=0.5)
cv2.imwrite(fname,image)
cam = cv2.VideoCapture(0)
ms = gpiozero.MotionSensor(4)
ms.when_motion = take_picture
take_picture()
signal.pause()
旧版のコード: こちらもセンサに反応があるとカメラを起動するところまでは同じですが、写真(jpg)ではなくビデオ(h264)を保存します(Raspberry5ではPiCameraが使えないため、新版は少し妥協した実装になっています…)。
import RPi.GPIO as GPIO
import time
import picamera
import datetime # new
def get_file_name(): # new
return datetime.datetime.now().strftime("%Y-%m-%d_%H.%M.%S.h264")
sensor = 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(sensor, GPIO.IN, GPIO.PUD_DOWN)
previous_state = False
current_state = False
cam = picamera.PiCamera()
while True:
time.sleep(0.1)
previous_state = current_state
current_state = GPIO.input(sensor)
if current_state != previous_state:
new_state = "HIGH" if current_state else "LOW"
print("GPIO pin %s is %s" % (sensor, new_state))
if current_state:
fileName = get_file_name() # new
cam.start_preview()
cam.start_recording(fileName) # new
else:
cam.stop_preview()
cam.stop_recording() # new
以上で、完成です。終了する場合は、gpio-pir.pyと同様、「Ctrl + C」です。
公式サイトには、カメラのLEDをOFFにする方法などもう少し詳しい解説があります。URLは次章の活用事例および巻末の参考資料に掲載しています。
サーボモータは、普通の(くるくると回転する)モータとは異なり、角度を指定して軸を回転させることができるモータです。ラジコンのハンドルや舵を制御するのに使われている、と言うとピンと来る方もいらっしゃるかもしれません。ロボットの関節なんかの駆動にも使われたりします。このモータを制御するにはPWM信号というパルスを生成してあげる必要があります。
必要なもの
今回使用したサーボモータは、SG90 MicroServo という機種です(写真参照)。駆動用の電源は単三電池を4つ繋いだ電池パックを使用しています。VccとGroundを電池に接続し、PWMをGPIOの4番ピンに接続します。Groundはさらに、GPIO側のGround端子とも接続が必要です。Raspberry Pi本体の5V端子から電源を取ると、電力不足で本体が落ちてしまうという情報があったので、このような配線にしていますが、短時間の駆動であれば(直接電源を取っても)問題なさそうです。電池パックがない場合、5V端子から電源を取るように配線してください。
PWM = 橙(ほとんど黄)
Vcc = 赤(ほとんど橙)
Ground = 茶
これで準備完了です。gpiozeroを使ったプログラムは以下のようになりますが、公式ドキュメントにも注意書きされているように、ちょっと実用化には向かないレベルで余計な振動が混ざります。
import gpiozero
from time import sleep
servo = gpiozero.Servo(4)
while True:
servo.min()
sleep(2)
servo.mid()
sleep(2)
servo.max()
sleep(2)
旧版のコード:
import time
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
gp_out = 4
GPIO.setup(gp_out, GPIO.OUT)
servo = GPIO.PWM(gp_out, 50)
servo.start(0.0)
for i in range(10):
servo.ChangeDutyCycle(2.5)
time.sleep(0.5)
servo.ChangeDutyCycle(12.0)
time.sleep(0.5)
GPIO.cleanup()
ChangeDutyCycleというメソッドで回転角を制御します。この値の範囲を求めるにはPWMの仕組みを理解する必要がありますが、とりあえず、今回のモータ(SG90)の場合は、2.5から12の間です。また、同様の理由で、波形を変更した後、モータが目的の位置まで回転する時間だけプログラムを待機してあげる必要があります。このプログラムでは 0.5秒間 ずつ待機しています。
参考記事:
- Raspberry
Piでサーボモータ(SG90)を制御する - ラズパイでラジコン制作(pigpio利用例)
ここまでの技術の応用ではありますが、測距センサを使ったプログラムの例も紹介します。写真の右側に写っているのが今回使用しているセンサ(Sharp 2Y0A21)です。数十センチのオーダーにある物体までの距離を測るのが本来の用途ですが、今回はGPIOの4番ピンに測定端子をつないで、ON/OFFのみの検出に利用しています。なぜか、赤い線がGroundで、黒い線が5V端子です。
プログラムは以下のようになります。0.1秒ごとに4番ピンの状態を監視し、ONになった瞬間のみ、カメラを起動し、タイムスタンプを名前に付けて画像を保存しています。
import RPi.GPIO as GPIO
import time, datetime, subprocess
sensor = 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(sensor, GPIO.IN, GPIO.PUD_DOWN)
previous_state = False
current_state = False
while True:
time.sleep(0.1)
previous_state = current_state
current_state = GPIO.input(sensor)
if current_state != previous_state:
new_state = "HIGH" if current_state else "LOW"
print("GPIO pin %s is %s" % (sensor, new_state))
if current_state:
f = datetime.datetime.now().strftime("%FT%T") + ".jpg"
print(f)
cmd = ["raspistill","-o",f,"-vf","-hf","-n","-t","1"]
subprocess.call(cmd)
raspistillの「-t」オプションの値を調整することで、センサ感知後どれだけの間をおいて撮影するかを調整することができます。「-vf」「-hf」は設置位置と向きに応じて適宜調整してください。
4本足のLEDを光らせてみましょう。一番長い足がGNDで、その他の3本がそれぞれ、R(赤),B(青),G(緑)の3原色に対応しています(R,GND,B,Gの順)。以下のコードはGPIO Zeroの基本レシピからの抜粋です。回路図は公式サイトを参照してください。
from gpiozero import RGBLED
from time import sleep
led = RGBLED(red=2, green=3, blue=4)
led.red = 1 # full red
sleep(1)
led.red = 0.5 # half red
sleep(1)
led.color = (0, 1, 0) # full green
sleep(1)
led.color = (1, 0, 1) # magenta
sleep(1)
led.color = (1, 1, 0) # yellow
sleep(1)
led.color = (0, 1, 1) # cyan
sleep(1)
led.color = (1, 1, 1) # white
sleep(1)
led.color = (0, 0, 0) # off
sleep(1)
# slowly increase intensity of blue
for n in range(100):
led.blue = n/100
sleep(0.1)
(GPIO Zeroの基本レシピからの抜粋です)
RaspberryPiではアナログ値を直接読み取る機能がないため、ADコンバータMCP3208を利用します。これまでの参考書ではSPI経由でデジタル値を取得するところから自前で実装する必要があったため、どうしても取っ付きにくい印象がありましたが、gpiozeroでは、様々なADコンバータに直接対応したクラスがあらかじめ用意されており、これを使って簡単に値を取得することができます。
コード(mcp3208.py)は以下のようになります。
from gpiozero import MCP3208
from time import sleep
pot = MCP3208(0)
while True:
print(pot.value)
sleep(0.5)
前節のフルカラーLEDの回路と組み合わせて可変抵抗を回すと色が変わるような仕掛けを作ってみましょう。
from gpiozero import RGBLED, MCP3208
led = RGBLED(red=2, green=3, blue=4)
red_pot = MCP3208(channel=0)
green_pot = MCP3208(channel=1)
blue_pot = MCP3208(channel=2)
while True:
led.red = red_pot.value
led.green = green_pot.value
led.blue = blue_pot.value
可変抵抗の代わりに各種センサを取り付けることができます。
上記のコードを少し改造して、例えば”部屋が明るくなった/暗くなった”時のように、状態が変わった時だけメッセージを表示するコードの例です。外部のシステムと連携する場合に全ての測定値を送信するのは現実的ではないケースなどで役に立つと思います。(実際はもう少しノイズの対応など必要になるかもしれません)
from gpiozero import MCP3208
from time import sleep
pot = MCP3208(0)
thresh_value = 0.5
prev_state = False
while True:
v = pot.value
if v > thresh_value:
new_state = True
else:
new_state = False
if new_state != prev_state:
print("%d -> %d (%.2f)" % (prev_state,new_state,v))
prev_state = new_state
sleep(0.5)
gpiozeroのレシピではSN754410というドライバを利用した例が紹介されています(下記URL)が、24年10月現在、秋月電気通商などでは終売となっていて調達ができません。代わりにDRV8835というドライバを使います。
コードは同じものを使いまわせます。ただし、筆者の環境では何故かGPIO4が常に”GPIO
Busy”というエラーになってしまい使えなかったため、代わりに
forward=2 を使っています。
from gpiozero import Motor
from time import sleep
motor = Motor(forward=2, backward=14)
while True:
motor.forward()
sleep(5)
motor.backward()
sleep(5)
以下の図を参考に、VM,GND=モーターの電源(+と-)、AOUT1,2=モーターA、BOUT1,2=モーターB、VCC=RaspberryPiの3.3V電源、AIN1,2=モーターA制御用のGPIO(本例では2と14番)、BIN1,2=モーターB制御用のGPIO(本例では未接続)と接続してください。
このドライバは2つのモーターを同時に制御することが可能です。
ADT7410という温度センサからの値を取得するプログラムです(gpio-adt7410.py)。保存して実行すると1秒ごとに測定した温度を表示します。
import smbus
def read_adt7410():
b = smbus.SMBus(1)
d = b.read_word_data(0x48,0x00)
d = (d & 0xff00)>>8 | (d & 0xff) <<8
d = d >> 3
if d & 0x1000 == 0:
t = d * 0.0625
else:
t = ((~d&0x1fff) + 1) * -0.0625
return t
import time
try:
while True:
v = read_adt7410()
print(v)
time.sleep(1.0)
except KeyboardInterrupt:
pass
このセンサとの接続にはI2Cという方式を使います。gpiozeroもI2Cに対応できるようなことは書かれているのですが、残念ながら手軽に使えそうなクラスは見当たりませんでした。そこで、以前から使える
smbus
というライブラリを代わりに使用しています。先に出てきたSPI通信に比べると、I2Cのセンサは配線が幾分シンプルで慣れやすいメリットはあるかもしれません。VCCとGNDの他に2本の信号線をそれぞれ、SDA=GPIO2,SCL=GPIO3と繋げば完成です(次節のLCDディスプレイの説明中に写真があります)。
また注意点として、あらかじめ RaspberryPi Config(設定) →
Interface(インターフェース) において I2C
を有効化しておかなければなりません。正しく有効化されているかどうかの確認には
i2cdetect -y 1
というコマンドを使うと便利です(有効な場合、接続されているセンサのアドレス、上記の場合48が表示されます)。
取り出した値(ビット列)を加工して摂氏温度に変換する過程は省略しています。詳しく知りたい方は以下の書籍などを参照してみてください。
もう一つ、I2C接続のパーツを紹介します。LCD(小型の液晶ディスプレイ)です。
こちらのコードはかなり大掛かりなものなのですが、、同じ書籍から引用させて頂きました。gpio-st7032.pyとして保存してください。実行すると、液晶ディスプレイに温度センサからの値が表示されます。
# This script was copied from:
# ラズパイ4対応 カラー図解 最新 Raspberry Piで学ぶ電子工作 作る、動かす、しくみがわかる! 講談社
import smbus
import sys, time
def read_adt7410():
b = smbus.SMBus(1)
d = b.read_word_data(0x48,0x00)
d = (d & 0xff00)>>8 | (d & 0xff) <<8
d = d >> 3
if d & 0x1000 == 0:
t = d * 0.0625
else:
t = ((~d&0x1fff) + 1) * -0.0625
return t
def setup_st7032():
trials = 5
for i in range(trials):
try:
c_lower = (contrast & 0xf)
c_upper = (contrast & 0x30)>>4
bus.write_i2c_block_data(address_st7032, register_setting, [0x38, 0x39, 0x14, 0x70|c_lower, 0x54|c_upper, 0x6c])
time.sleep(0.2)
bus.write_i2c_block_data(address_st7032, register_setting, [0x38, 0x0d, 0x01])
time.sleep(0.001)
break
except IOError:
if i==trials-1:
sys.exit()
def clear():
global position
global line
position = 0
line = 0
bus.write_byte_data(address_st7032, register_setting, 0x01)
time.sleep(0.001)
def newline():
global position
global line
if line == display_lines-1:
clear()
else:
line += 1
position = chars_per_line*line
bus.write_byte_data(address_st7032, register_setting, 0xc0)
time.sleep(0.001)
def write_string(s):
for c in list(s):
write_char(ord(c))
def write_char(c):
global position
byte_data = check_writable(c)
if position == display_chars:
clear()
elif position == chars_per_line*(line+1):
newline()
bus.write_byte_data(address_st7032, register_display, byte_data)
position += 1
def check_writable(c):
if c >= 0x06 and c <= 0xff :
return c
else:
return 0x20 # 空白文字
bus = smbus.SMBus(1)
address_st7032 = 0x3e
register_setting = 0x00
register_display = 0x40
contrast = 32 # 0から63のコントラスト。30から40程度を推奨
chars_per_line = 8 # LCDの横方向の文字数
display_lines = 2 # LCDの行数
display_chars = chars_per_line*display_lines
position = 0
line = 0
setup_st7032()
try:
while True:
inputValue = read_adt7410()
try:
clear()
s = "Temp- "+str(inputValue)
write_string(s)
except IOError:
print("can't connect")
time.sleep(1)
except KeyboardInterrupt:
pass
以下の写真は、温度センサとこのLCDを同時に接続した様子です。
こちらを応用して、さまざまな値をディスプレイに表示してみましょう!可変抵抗,照度センサー,CPU温度etc… 例えばCPUのロードアベレージを表示する場合、inputValue = ..の列を以下のように書き換えます。
inputValue = "%.3f" % (os.getloadavg()[0])
センサを組み合わせる場合、前節のADコンバータ(MCP3208)のコードを利用できます。浮動小数展をそのまま表示すると桁数が足りなくなってしまいますので、適宜丸め(四捨五入等)を行なって調整してください。
pot = MCP3208(0)
:
inputValue = round(pot.value,3)
TODO: 一通り実践してみる(人間が担当、Copilotのタスクではありません) 前章まで扱ってきた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を選んでおくと良いでしょう。
プログラミングにはMicroPythonを使います。MicroPythonはマイコン向けに設計された小型のPython処理系で、Pythonの構文をほぼそのまま使えます。前章まででPythonに触れていれば、すんなり入れるはずです。
Pico WをRaspberry PiのUSBポートに接続します。Raspberry Pi OSにはThonnyというPython用のIDEが標準インストールされており、MicroPythonのPico向け対応も組み込まれているため、追加インストールは不要です。
Thonnyを起動し、画面右下のドロップダウンで 「MicroPython (Raspberry Pi Pico)」 を選びます。
接続の確認
Thonny下部のシェルペイン(「Shell」タブ)に以下のように表示されれば、PicoのMicroPythonと通信できています。
MicroPython v1.26.1 on 2025-09-11; Raspberry Pi Pico W with RP2040
Type "help()" for more information.
>>>
>>> のプロンプトの後に
print("hello")
と入力してEnterを押してみてください。hello
と返ってきたら準備完了です。
トラブルシュート:MicroPythonが起動しない場合
購入したPico WによってはMicroPythonがプリインストールされていないことがあります。その場合は以下の手順でファームウェアを書き込みます。
RPI-RP2)としてファイルマネージャに表示されますRaspberry Pi Pico W
になっていることを確認して「インストール」を実行しますこれ以降は、ただUSBに挿すだけで認識されます(BOOTSELを押す必要はありません)。
最初のプログラムとして、Pico W本体に内蔵されているLEDを点滅させてみましょう。外付けの部品は不要です。
Thonnyの上半分がコードエディタ、下半分がShellです。コードエディタに以下を入力し、「実行」ボタン(▶)を押してください。
import machine
import time
led = machine.Pin("LED", machine.Pin.OUT)
for i in range(5):
led.on()
time.sleep(1)
led.off()
time.sleep(1)Pico W本体の緑色のLEDが1秒間隔で5回点滅すれば成功です。
machine
はPicoのハードウェアを操作するためのモジュールです。machine.Pin("LED", machine.Pin.OUT)
でPico
W内蔵のLEDに対応するピンを「出力モード」で取得しています。led.on()
で点灯、led.off() で消灯し、time.sleep(1)
で1秒待機しています。range(5) を range(10)
に変えれば10回点滅します。
ずっと点滅させ続けるには
for ループを while True:
に変えると、停止するまで永遠に繰り返します。
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の「停止」ボタン(■)を押してください。
内蔵LEDの点滅が確認できたら、外付けのLEDとボタンをGPIOピンに繋いでみましょう。前章(with GPIO)と同じ部品が使えます。
外付けLEDの点滅
以下の回路を組んでください。LEDのアノード(長い足)を1kΩ抵抗を介してGP15(ピン番号20)に、カソード(短い足)をGND(ピン番号38)に接続します。
TODO: Pico WにLEDとボタンを繋いだ回路図を追加する
import machine
import time
led = machine.Pin(15, machine.Pin.OUT)
while True:
led.on()
time.sleep(0.5)
led.off()
time.sleep(0.5)前章(with GPIO)のRaspberry Pi版(gpiozero)と比べてみましょう。
| Raspberry Pi(gpiozero) | Pico W(MicroPython) | |
|---|---|---|
| ライブラリ | import gpiozero |
import machine |
| ピン取得 | gpiozero.LED(4) |
machine.Pin(15, machine.Pin.OUT) |
| 点灯 | led.on() |
led.on() |
| 消灯 | led.off() |
led.off() |
on() / off()
の操作は同じですが、ライブラリとピン指定の方法が異なります。Picoではmachine.Pinに対してピン番号と方向(OUTまたはIN)を明示します。
ボタンの押下検出
次にボタンを追加します。タクトスイッチの片側をGP14(ピン番号19)に、もう片側をGND(ピン番号38)に接続してください。
import machine
import time
led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
while True:
if button.value() == 0: # ボタンが押されている(GNDに落ちている)
led.on()
else:
led.off()
time.sleep(0.05)machine.Pin.PULL_UP
は内部プルアップ抵抗を有効にする設定です。ボタンを押すとGNDに繋がって
value() が 0 になり、離すと 1
に戻ります。この「押している間だけ点灯する」動作を確認してみてください。
前章(with GPIO)のPi版では gpiozero.Button
がプレスとリリースのイベントを自動で扱ってくれましたが、Picoではポーリング(一定間隔で状態を確認するループ)で実装するのが基本です。よりシンプルな処理で確実に動くのがマイコンらしい書き方です。
前章では、Raspberry PiにはアナログInputが備わっていないため外付けのADコンバータ(MCP3208)をSPI経由で使いました。PicoにはADC(アナログ-デジタルコンバータ)が内蔵されているため、外付けチップなしに可変抵抗や照度センサなどのアナログ電圧を直接読み取ることができます。
ADCに対応したピンはGP26(ADC0)、GP27(ADC1)、GP28(ADC2)の3本です(さらに内蔵温度センサ用のADC4が存在しますが、外部ピンには出ていません)。今回はGP26に可変抵抗(B10kΩ)を接続します。
用意するもの
可変抵抗の3本の端子のうち、両端の2本をそれぞれ3.3V(ピン番号36)とGND(ピン番号38)に、中央の端子(ワイパー)をGP26(ピン番号31)に接続します。
import machine
import time
adc = machine.ADC(0) # GP26 = ADC0
while True:
value = adc.read_u16() # 0 〜 65535 の整数値
voltage = value * 3.3 / 65535 # 電圧に換算(0〜3.3V)
print(f"Raw: {value}, Voltage: {voltage:.2f}V")
time.sleep(0.5)machine.ADC(0)
でADC0(GP26)を取得し、read_u16()
で16ビット値(0〜65535)を読みます。可変抵抗を回すにつれ、値が変化するのを確認してください。
| Raspberry Pi(gpiozero) | Pico W(MicroPython) | |
|---|---|---|
| 外付けADC | MCP3208(SPIで接続) | 不要(内蔵ADC) |
| 読み取り | pot.value(0.0〜1.0) |
adc.read_u16()(0〜65535) |
前章では gpiozero.RGBLED
を使って色を手軽に制御しましたが、PicoのMicroPythonでは
machine.PWM
を使って各チャンネルを直接制御します。PWMの周波数は1000Hz、デューティ比の指定は
duty_u16() で0〜65535の整数で行います。
用意するもの
GP16(赤)、GP17(緑)、GP18(青)にそれぞれ330Ω抵抗を介してLEDを接続します。カソード(共通の最も長い足)はGND(ピン番号38)に繋ぎます。
import machine
import time
red = machine.PWM(machine.Pin(16))
green = machine.PWM(machine.Pin(17))
blue = machine.PWM(machine.Pin(18))
for ch in (red, green, blue):
ch.freq(1000)
def set_color(r, g, b):
"""r, g, b はそれぞれ 0.0〜1.0 の浮動小数"""
red.duty_u16(int(r * 65535))
green.duty_u16(int(g * 65535))
blue.duty_u16(int(b * 65535))
# 基本6色を順に切り替え
colors = [
(1, 0, 0), # 赤
(0, 1, 0), # 緑
(0, 0, 1), # 青
(1, 1, 0), # 黄
(0, 1, 1), # シアン
(1, 0, 1), # マゼンタ
]
while True:
for r, g, b in colors:
set_color(r, g, b)
time.sleep(1)可変抵抗と組み合わせる
前節のADC回路に加えて、GP27とGP28にも可変抵抗を追加すると、各色を独立して調整できます。
import machine
import time
red = machine.PWM(machine.Pin(16))
green = machine.PWM(machine.Pin(17))
blue = machine.PWM(machine.Pin(18))
for ch in (red, green, blue):
ch.freq(1000)
adc_r = machine.ADC(0) # GP26
adc_g = machine.ADC(1) # GP27
adc_b = machine.ADC(2) # GP28
while True:
red.duty_u16(adc_r.read_u16())
green.duty_u16(adc_g.read_u16())
blue.duty_u16(adc_b.read_u16())
time.sleep(0.05)前章の gpiozero.RGBLED
はドライバが自動で各色をPWM制御してくれましたが、Picoでは自分で
machine.PWM
を組み合わせることで同じことができます。仕組みが見えるので、PWMの動きを理解する良い練習にもなります。
前章ではRaspberry PiのRPi.GPIOを使って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 |
import machine
import time
servo = machine.PWM(machine.Pin(15))
servo.freq(50) # 50Hz = 20ms周期
def set_angle(angle):
"""0〜180 の角度をデューティ比に変換"""
pulse_us = 500 + int(angle / 180 * 1900) # 500〜2400 µs
duty = int(pulse_us / 20000 * 65535)
servo.duty_u16(duty)
while True:
set_angle(0)
time.sleep(1)
set_angle(90)
time.sleep(1)
set_angle(180)
time.sleep(1)前章の RPi.GPIO.PWM.ChangeDutyCycle()
は0〜100のパーセント値を渡していましたが、MicroPythonでは
duty_u16() に0〜65535の整数を渡します。計算式を
set_angle() 関数にまとめておくと使い回しが楽です。
DCモータを直接GPIOに繋ぐと過電流でPicoが破損する恐れがあるため、モータードライバを間に入れます。ここでは代表的な L298N モジュールを使います。
用意するもの
接続
| L298N端子 | Pico W接続先 |
|---|---|
| IN1 | GP20(ピン番号26) |
| IN2 | GP21(ピン番号27) |
| ENA | GP22(ピン番号29) |
| GND | GND(ピン番号38) |
| +12V(または+5V) | 外部電源 |
L298NのENAにはPWMでスピードを制御します。IN1とIN2の組み合わせで回転方向を決めます。
| IN1 | IN2 | 動作 |
|---|---|---|
| HIGH | LOW | 正転 |
| LOW | HIGH | 逆転 |
| LOW | LOW | 停止 |
import machine
import time
IN1 = machine.Pin(20, machine.Pin.OUT)
IN2 = machine.Pin(21, machine.Pin.OUT)
ENA = machine.PWM(machine.Pin(22))
ENA.freq(1000)
def forward(speed=0.5):
IN1.on()
IN2.off()
ENA.duty_u16(int(speed * 65535))
def backward(speed=0.5):
IN1.off()
IN2.on()
ENA.duty_u16(int(speed * 65535))
def stop():
IN1.off()
IN2.off()
ENA.duty_u16(0)
forward(0.7) # 70%のスピードで正転
time.sleep(2)
stop()
time.sleep(0.5)
backward(0.5) # 50%のスピードで逆転
time.sleep(2)
stop()speed は0.0〜1.0で指定します。forward(1.0)
がフル回転、forward(0.3)
が低速です。前節のサーボと組み合わせれば、前進・後退・方向転換ができる簡単な自走ロボットに発展させることもできます。
Picoにはチップ内部に温度センサが内蔵されており、外付けセンサなしで気温に近い値を取得できます(あくまでチップ温度なので周囲温度とは数℃程度ずれることがあります)。内蔵センサはADCチャンネル4(machine.ADC(4))として読み取れます。
import machine
import time
sensor = machine.ADC(4)
while True:
reading = sensor.read_u16()
voltage = reading * 3.3 / 65535
# RP2040データシートの変換式
temperature = 27 - (voltage - 0.706) / 0.001721
print(f"チップ温度: {temperature:.1f}°C")
time.sleep(1)変換式は公式データシートに記載されているもので、電圧値から摂氏温度を求めています。
前章と同じ
ADT7410(I2Cインターフェース)を使うこともできます。PicoではI2Cもハードウェアサポートされており、machine.I2C
で扱えます。接続はSDA/SCLの2本で完結し、SDA=GP4(ピン番号6)、SCL=GP5(ピン番号7)と繋ぎます。Raspberry
Pi側で必要だった i2cdetect
による事前確認は不要で、コードから直接通信できます。
import machine
import time
i2c = machine.I2C(0, sda=machine.Pin(4), scl=machine.Pin(5))
ADT7410_ADDR = 0x48
while True:
data = i2c.readfrom_mem(ADT7410_ADDR, 0x00, 2)
raw = (data[0] << 8 | data[1]) >> 3
if raw >= 4096:
raw -= 8192 # 負の温度(2の補数表現)
temperature = raw / 16
print(f"温度: {temperature:.1f}°C")
time.sleep(1)前章では smbus
ライブラリを使っていましたが、MicroPythonでは追加ライブラリなしで
machine.I2C
がそのまま使えます。16ビット(2バイト)のレジスタを読み出し、上位13ビットを取り出して温度に変換しています。
| Raspberry Pi | Pico W | |
|---|---|---|
| I2Cライブラリ | smbus | machine.I2C(内蔵) |
| 読み取り | bus.read_word_data() |
i2c.readfrom_mem() |
| 初期設定 | i2cdetect で動作確認 |
不要 |
I2C接続のLCDを machine.I2C で制御します。ここでは市販の
LCD1602モジュール(PCF8574 I2Cバックパック搭載)
を使います。このバックパックは16ピンの並列LCD(HD44780互換)をI2C2本の配線に変換してくれるアダプタで、配線がシンプルで扱いやすいのが特徴です。
用意するもの
接続(LCDモジュール → Pico W)
| LCD端子 | Pico W接続先 |
|---|---|
| VCC | 3.3V(ピン番号36) |
| GND | GND(ピン番号38) |
| SDA | GP4(ピン番号6) |
| SCL | GP5(ピン番号7) |
PCF8574のデフォルトI2Cアドレスは 0x27
ですが、製品によっては 0x3F
の場合もあります。まずは以下のコードで接続されているアドレスを確認しましょう。
import machine
i2c = machine.I2C(0, sda=machine.Pin(4), scl=machine.Pin(5))
print(i2c.scan()) # 検出されたアドレスを表示[39] と表示されれば
0x27(39の16進数)です。
LCDを操作するシンプルなドライバ
PCF8574経由でLCDを制御するには、ニブル(4ビット)モードでコマンドを送る必要があります。以下に最小限のドライバクラスを示します。
import machine
import time
class LCD:
def __init__(self, i2c, addr=0x27, cols=16, rows=2):
self.i2c = i2c
self.addr = addr
self._bl = 0x08 # バックライト ON
self._init()
def _send_byte(self, byte, mode):
high = (byte & 0xF0) | self._bl | mode
low = ((byte << 4) & 0xF0) | self._bl | mode
for nibble in (high, low):
self.i2c.writeto(self.addr, bytes([nibble | 0x04]))
time.sleep_us(1)
self.i2c.writeto(self.addr, bytes([nibble & ~0x04]))
time.sleep_us(50)
def _cmd(self, cmd): self._send_byte(cmd, 0x00)
def _char(self, char): self._send_byte(ord(char), 0x01)
def _init(self):
time.sleep_ms(50)
for _ in range(3):
self._send_byte(0x30, 0x00)
time.sleep_ms(5)
self._cmd(0x20) # 4ビットモード
self._cmd(0x28) # 2行・5x8ドット
self._cmd(0x0C) # ディスプレイON、カーソルOFF
self._cmd(0x06) # エントリモード:左から右
self._cmd(0x01) # クリア
time.sleep_ms(2)
def clear(self):
self._cmd(0x01)
time.sleep_ms(2)
def move(self, col, row):
offsets = [0x00, 0x40]
self._cmd(0x80 | (col + offsets[row]))
def write(self, text):
for ch in text:
self._char(ch)
i2c = machine.I2C(0, sda=machine.Pin(4), scl=machine.Pin(5))
lcd = LCD(i2c)
lcd.clear()
lcd.move(0, 0) # 1行目・0列目
lcd.write("Hello Pico!")
lcd.move(0, 1) # 2行目・0列目
lcd.write("MicroPython")温度センサと組み合わせる
前節の内蔵温度センサと組み合わせると、1秒ごとに温度をLCDに表示するデジタル温度計が作れます。
import machine
import time
# LCD クラスの定義は上記の通り(省略)
i2c = machine.I2C(0, sda=machine.Pin(4), scl=machine.Pin(5))
lcd = LCD(i2c)
sensor = machine.ADC(4)
while True:
reading = sensor.read_u16()
voltage = reading * 3.3 / 65535
temperature = 27 - (voltage - 0.706) / 0.001721
lcd.clear()
lcd.move(0, 0)
lcd.write("Temperature:")
lcd.move(0, 1)
lcd.write(f"{temperature:.1f} C")
time.sleep(1)前章ではST7032
LCDのドライバが複雑で書籍から引用する形でしたが、LCD1602 +
PCF8574の構成ではドライバのロジックを追いやすく、machine.I2C
の使い方をそのまま学べます。
これまでのプログラムはThonnyから実行していましたが、Picoにプログラムを書き込んでおくと、電源を入れると自動的に起動します。Picoが起動時に自動実行するファイルは
main.py です。
手順
main.py
として保存しますこの状態でPicoにバッテリーを繋げば、ThonnyやRaspberry Piなしに単体で動作します。
電源の繋ぎ方
単三電池3本(1.5V × 3 = 4.5V)を VSYSピン(ピン番号39)とGND(ピン番号38) に接続します。PicoのVSYSは1.8〜5.5Vに対応しており、Pico自体の3.3V電源レギュレータが内部で変換してくれます。USB電源バンクをMicro USBポートに繋ぐのもシンプルな方法です。
| 電源 | 接続先 | ポイント |
|---|---|---|
| 単三電池 × 3 | VSYS + GND | 3本で4.5V、軽量 |
| USB電源バンク | Micro USB | USB規格のまま接続 |
実行中にプログラムを変更したい場合
再びUSBでRaspberry PiとつないでThonnyを起動すると、保存済みの
main.py
を上書きして更新できます。BOOTSELを押す必要はありません。
mpremote はPCやRaspberry PiからPicoをコマンドラインで操作するツールです。Thonnyを開かなくても、ファイルのコピーや簡単なコマンド実行がターミナルだけで完結します。
インストール
Raspberry Pi上で以下を実行します。
pip install mpremote
主なコマンド
| コマンド | 意味 |
|---|---|
mpremote connect list |
接続中のPicoを一覧表示 |
mpremote |
REPLを起動(Ctrl+Xで終了) |
mpremote run blink.py |
ローカルのblink.pyをPicoで実行 |
mpremote fs ls |
Pico上のファイル一覧 |
mpremote fs cp main.py :main.py |
ローカルのファイルをPicoにコピー |
mpremote exec "print('hello')" |
1行のコードをすぐ実行 |
:(コロン)プレフィックスがPico側のパスを表します。例えば
mpremote fs cp :main.py main.py
はPicoから手元にコピーする操作です。
典型的な使い方
# 接続デバイスを確認
mpremote connect list
# ファイルを転送して実行
mpremote fs cp blink.py :blink.py
mpremote run :blink.py
# 直接コマンドを試す
mpremote exec "import machine; machine.Pin(15, machine.Pin.OUT).on()"
Thonnyとの使い分けとしては、「コードを育てる段階ではThonny、完成した後のデプロイやデバッグにはmpremote」という使い方が便利です。
PicoRuby はマイコン向けの軽量Ruby実装で、PicoのうえでRubyのコードを動かすことができます。MicroPythonが使えるようになって「Rubyでも書けたら」と思うなら試してみる価値はあります。ただし、2026年3月時点、Pico Wへのインストール時に問題が発生するようです(Wifi機能なしのPicoでは動作)。
インストール
MicroPythonと同様、UF2ファイルをPicoに書き込んでインストールします。
picoruby-pico_w.uf2 をダウンロードしますRPI-RP2
ストレージが現れたらUF2ファイルをコピーします基本的な動作を確認する
mpremoteやシリアルターミナルでPicoに接続すると、Rubyのプロンプトが現れます。
puts "Hello from PicoRuby!"
# LEDを点滅させる
led = GPIO.new(25, GPIO::OUT)
5.times do
led.write(1); sleep 0.5
led.write(0); sleep 0.5
endMicroPythonとの比較
| MicroPython | PicoRuby | |
|---|---|---|
| 言語 | Python | Ruby |
| コミュニティ | 非常に大きい | 小さい(発展途上) |
| サードパーティライブラリ | 豊富 | 少ない |
| 標準ライブラリ | ほぼ網羅 | 一部のみ |
| 公式ドキュメント | 充実 | 整備途上 |
PicoRubyはMicroPythonに比べるとライブラリが少なく、センサドライバや通信プロトコルの実装を自前で書く場面も多いです。「Rubyの文法で書きたい」という強い動機がある場合の選択肢と考えておくといいでしょう。まずはMicroPythonで基礎を固め、余裕ができたらPicoRubyを試してみてください。
この章ではRaspberryPiをウェブアプリケーションサーバとして利用する方法を解説します。ここで利用するフレームワークは[Ruby on Rails]です。拙著、Ruby on Railsで作るウェブアプリケーション(Rails7対応版)の内容に近いものをRaspberryPi環境向けに書き変えた構成になっています。Rails8に対応した新しいテキスト Dive into Ruby on Rails 2025 も現在鋭意準備中です。
まずは必要なパッケージを導入します。
sudo apt-get -y install rbenv ruby-dev libyaml-dev libpq-dev
rbenvはRubyの実行環境を管理するツールで、これを使ってrubyをインストールします。デフォルトのパッケージでは(2025.08時点)かなり古い3.1系がインストールされてしまうので、一部分(ruby-buildプラグイン)だけ新しくします。
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 3.4.7
rbenv global 3.4.7
rbenv init # ここに出てくる指示に従って設定ファイルを書き換えます
(rbenv init
の案内が難しい方向け)以下のコマンドでインストール可能です。実行後に、今回インストールしたバージョン(3.4.7)が選択されていることを確認してください。
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
. ~/.bashrc
rbenv versions # systemではなく、3.4.7が選択されていることを確認
続けて、railsをインストールします。
gem update --system
gem install pg rails -N
rbenv rehash
それぞれのコマンドを起動してバージョンを確認してください。本書では以下の構成で解説を進めますが、マイナーバージョン(末尾の数値)が異なっても、大抵のケースでは問題はないはずです。
ruby -v # ruby 3.4.7 (2025-10-08..
rails -v # Rails 8.1.1
準備ができたら以下のコマンドで新しいプロジェクト(rails01-rooms)を作成し、その中に移動(cd)してください。
rails new rails01-rooms
cd rails01-rooms
lsコマンドやFile Managerで中身をのぞいてみてください。大量のファイルとディレクトリが作成されているはずです。この後、使うものを抜粋して掲載します。
以下のコマンドでウェブサーバを起動します。
bundle exec rails server
RaspberryPi上でブラウザを起動して以下のアドレスにアクセスし、以下の画面が見えたらここまで順調です!
http://localhost:3000
サーバを終了したい場合は「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で終了、上記コマンドで起動しなおします)。
準備が整ったところで、さっそくモデル(データを保存する入れ物のようなもの)を作っていきましょう。今回は会議室の利用状況を把握するための、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
次に、入退室の状態変更を外部から受け付けるためのAPI(application interface)を作成します。
部屋の名前をキーにして指定できるようにしたいので、名前が一意になるようにモデルに制約を追加します。
app/models/room.rb
を編集して以下の1行を足してください。
validates :name, uniqueness: true
config/routes.rb
にはAPIのパスを定義します。これは定義内の先頭(2行目)に書いてください(resouces文の下に書くと意図したように動きません)。
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」を「使用中」に更新することが可能です。Section1の指示通りにイメージを作っている場合、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というフレームワーク(部品のセット)を利用します。詳しい解説や作例は公式サイトを確認してください。ネットで色々見つかるかもしれませんが、バージョンごとに使えるクラス名がちょっとずつ異なるため、できる限り公式サイトの例を参考にした方がスムーズかと思います。
https://getbootstrap.com/docs/5.2/getting-started/introduction/
本書では執筆時点最新の安定版である5.2を利用しています。
app/views/layouts/application.html.erb
に以下のように、headタグの最後尾にlinkタグを、bodyタグの最後尾にscriptタグを追加します。CDNという仕組みを利用しているので、テスト中もインターネット接続が必要な点に留意してください。閉鎖環境で使う場合は、それぞれ必要なファイルをダウンロードして読み込む必要があります。
:
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" 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.2.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" 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" %>
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>
最後に、show.html.erb
のボタンにもスタイルをつけてみましょう。mt-2
というクラスは要素の余白をコントロールするためのもので、この場合、
margin t op を5段階中 2
番目のサイズ確保するという意味になります。他にも pb-5 =
padding top 5、とか mx-3 = marging x axis(start and end)
などなど様々な指定が可能です。参考リンク: 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" %>
その他、レイアウトの支援をしてくれるrowやcolumnの概念、クラス名で余白を制御する機能などなど、ウェブアプリの機能を実装する上で役にたつものが沢山揃っています。
公式サイトを参考にしながら、他のページにもスタイルを適用してみてください。
https://getbootstrap.com/docs/5.2/getting-started/introduction/
最後におまけで、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には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
開発用のサーバが起動していると失敗しますので、停止済みか確認した上で、以下のコマンドを実行します。
systemctl daemon-reload
systemctl enable puma-myapp
systemctl start puma-myapp
うまくいけば、サーバを再起動した後もウェブアプリが稼働していることを確認できるはずです。
プロセスがずっと起動した状態になっているため、構成を変更した後は以下の要領で再起動が必要です。
restartの代わりにstopを指定して停止すれば rails server
も再び利用できます。
sudo systemctl restart puma-myapp
当初はRails本と同じく、Phusion Passengerを使ってApacheと連携させる方式を紹介する予定でしたが、Raspberry Pi上では最新版のパッケージが提供されておらず、ビルドも困難であったため、Pumaを直接デーモン化するこの形を採用しています。
この章で作成したRailsプロジェクトは以下のリポジトリに公開しています。もしここまでの動作で不明なところがあれば、こちらを参考にして間違いがないか探してみてください。
実際に利用する場面を想定して、あるWifi環境下に設置した時に、ユーザがどのようにアクセスするのかという実用面から含めてアプリの流れを考えてみましょう。操作手順書のようなものを先に作ってしまって、それに合わせて実装を拡張するような流れも良いかもしれません。
障害対応、バックアップはどうしますか?1~2年というスパンで動かすことを考えた時にどんなメンテナンスが必要になるでしょうか?ほんの十数年前までは、中小企業の基幹システムと言えば、この程度のスペックのサーバを事務所の真ん中にドンと置いて大事に使う、というものが主流でした(今は大部分クラウドに移ったと思いますが)。
この章では、OpenCVという画像処理ライブラリとUltralyticsという機械学習による画像分類のためのライブラリの利用方法を紹介します。まずは、必要な環境を準備します(opencvは既に1章でも触れています)。
sudo apt install python3-opencv python3-full
cd
python3 -m venv ./venv
./venv/bin/pip3 install ultralytics
今までと異なるのは、2行目でvenvという仮想環境を作っている部分です。RaspberryPiOSの提供するパッケージに含まれないライブラリ(今回はultralytics)をインストールするために推奨されている方法です。上記のコマンドを実行するとHOME直下にvenvというディレクトリが作成され、その中にpython関連のコマンドが配置されます。
まずは、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による物体検出の結果を書き込んでみましょう。ファイル名は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()
以下のように画面に映った物体を検出して名前付きの枠で囲ってくれます。横にある数字は、どれくらいの確度でその物体であるかを表しています。
上記のプログラムで読み込んでいた’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()
上記のサンプルを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()
先にハイレベルなライブラリを紹介してしまいましたが、ここでコンピュータビジョンの先駆的なライブラリの紹介に戻りたいと思います。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()
ファイルとディレクトリは以下のような関係になります。
ここから先は、保存するファイル名 :
処理の説明に続けて実際のコードを記載します。サンプルのdog.jpgは各自適当なサンプルを用意してください。本書の結果サンプル用には以下の画像を使用しました。
opencv1-blur.py :
まずはシンプルな例です。画像をグレースケールで読み込んでぼかし(blur)フィルタを適用した結果を表示/保存します。
import cv2
import common
img1 = common.imread('dog.jpg',0)
img2 = cv2.blur(img1,(5,5))
common.imshow('blur',img1,img2)
以下のように実行します。
python3 opencv1-blur.py
何かキーを押すとウィンドウが閉じられ、表示された画像と同じものがresultsフォルダ内に保存されます。このとき、キーを押さずにマウスでウィンドウの閉じるボタンを押して終了してしまうと、pythonのプロセスが残ってしまうので注意してください(終了するにはkillコマンドを使う必要あります)。
opencv2-contours.py :
画像の輪郭を抽出するサンプルです。まず、二値化処理をした画像から輪郭(白と黒の境目にあたる部分の線分の集合)を取り出し、それを別の画像(img2)に描画しています。複雑ですが、古典的な物体検出の際にはよく使われるアプローチです。thresholdやfindContoursのパラメタ、あるいは読み込む画像を変えながらどのように結果が変化するか確認してみると理解が進むかもしれません。
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
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)
opencv4-colors.py : 色を扱うサンプルです。
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):
img1.itemset((y,x,0),x/2)
img1.itemset((y,x,1),255)
img1.itemset((y,x,2),255)
img2 = cv2.cvtColor(img1,cv2.COLOR_HSV2BGR)
common.imshow('colors',img1,img2)
opencv5-pixel.py :
グレースケールイメージの任意の箇所にピクセル単位で描画をするサンプルです。
import cv2
import common
img1 = common.imread('dog.jpg')
img2 = img1.copy()
v = img2.item(10,20)
print(v)
for y in range(10,20):
for x in range(20,40):
img2.itemset((y,x),(x+y)*4)
v = img2.item(10,20)
print(v)
v = img2.item(19,39)
print(v)
common.imshow('pixel',img1,img2)
opencv6-roi.py : ROI(region of
interest)という概念は色んな場面で登場します。CとPythonでは扱い方が違うので最初は戸惑うかもしれません。
import cv2
import common
img1 = common.imread('dog.jpg',0)
img2 = img1[100:180, 90:150]
cv2.rectangle(img2,(0,0),(59,79),255,3)
common.imshow('roi',img1,img2)
opencv7-split.py :
ROI同様、画像のレイヤ(RGB,HSVなど)の考え方も慣れが必要です。
import cv2
import common
img1 = common.imread('dog.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
opencv8-thresh.py :
輪郭抽出のサンプルでも出てきた二値化処理のサンプルです。THRESH_BINARYは最もシンプルなアルゴリズムですが、このほかにも幾つかのアルゴリズムが利用できます。THRESH_OTSU
が(筆者の経験の範囲では)多様な場面で”いい感じ”の処理をしてくれます。
import cv2
import common
img1 = common.imread('dog.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
OpenCVの公式ドキュメントを見ると、他にもまだまだ沢山のフィルタが存在しています。これらを使って、カメラの画像をリアルタイムに加工するアプリケーションを作ってみましょう。Yoloと組み合わせて、人物の特定の動作に合わせてフィルタを適用したり、画像を加工するなどの組み合わせでも遊べそうです。自由に課題を決めて何か作ってみてください。
YOLOによる人物検知システムが撮影した画像をブラウザ経由で閲覧、統計を見ることが出来るアプリを開発します。 以下の項目を予め理解していることを、前提として記載しています(HTMLの基本以外は本書内にも解説があります)。
この章では、前章で学んだYOLOを使って人物を検知し、検知した際に画像を自動保存するシステムを構築します。さらに、保存された画像をブラウザで閲覧できるようにSinatraというRubyのウェブフレームワークを使ってウェブアプリケーションを作成します。
以下のような機能を実装します:
完成すると以下のような画面で画像を閲覧できるようになります。時間帯別の検知回数を棒グラフで表示し、最新の検知画像とスナップショット画像を確認できます。
まずは前章で学んだYOLOを使って、人物を検知したときに画像を保存するプログラムを作成します。以下の準備が必要です(前章で既に実施済みの場合はスキップしてください)。
sudo apt install python3-opencv python3-full
cd
python3 -m venv ./venv
./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()このプログラムは以下の動作をします:
-changeが付く)motionsディレクトリに保存snapshotsディレクトリに保存このメカニズムにより、以下のような利点があります:
例えば、1人がずっと滞留している場合でも、入室時に1枚(人数変化)、退室時に2枚(人数変化+検知セッション終了時の最良フレーム)が保存されます。一時的な検出ミス(1人→2人→1人のような瞬間的な誤検知)は無視されます。
プログラムを実行してみましょう:
~/venv/bin/python3 detect_person.py
カメラの前で動いてみて、人物が検知されることを確認してください。画像ファイルが~/yolo_captures/motions/に保存されていれば成功です。
検知システムで保存された画像をブラウザで閲覧できるように、Webアプリケーションを作成します。今回は、シンプルなアプリを作るのに便利なSinatraというRubyのフレームワークを利用します。
まず、Sinatraをインストールします:
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 :public_folder, File.expand_path('~/yolo_captures')
get '/' do
erb :index
end
get '/motions' do
@image_type = "motions"
erb :images
end
get '/snapshots' do
@image_type = "snapshots"
erb :images
end
get '/activities' do
erb :activities
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: collapse;
}
table.activities td {
vertical-align: bottom;
font-size: 9px;
color: #ccc;
}
table.activities div {
width: 10px;
background-color: 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)。
システム全体の動作を確認してみましょう。
cd ~/yolo_detection
~/venv/bin/python3 detect_person.py
cd ~/webapp
ruby main.rb -o 0.0.0.0
カメラの前で動いて人物検知が動作することを確認し、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/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カメラの場合はsudo raspi-configで有効化されているか確認してください。
ディレクトリのパーミッション(権限)を確認してください:
ls -la ~/yolo_captures/
書き込み権限がない場合は以下のコマンドで修正します:
chmod 755 ~/yolo_captures
chmod 755 ~/yolo_captures/motions
chmod 755 ~/yolo_captures/snapshots
# main.rbに以下を追加してデバッグ
puts "Public folder: #{settings.public_folder}"Raspberry Pi 4以前のモデルでは、YOLOの処理に時間がかかる場合があります。以下の対策を検討してください:
このシステムをベースに、様々な拡張が可能です:
検知条件のカスタマイズ
特定の条件でのみ保存するように変更できます:
# 複数人が検知されたときのみ保存
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 :json
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
{
motions: { latest: motion_latest, count: motion_latest ? Dir.children(motion_dir).count : 0 },
snapshots: { latest: snapshot_latest, count: 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アプリケーションを組み合わせて、実用的な監視カメラシステムを構築しました。
学んだ内容:
この知識を応用することで、様々な画像認識システムを構築できます。例えば、特定の物体の検出、顔認識、ポーズ推定など、chapter6で学んだYOLOの機能を組み合わせることで、より高度なシステムを作ることができます。
HTML、CSS、Ruby、JavaScriptの詳細については、インターネット上に豊富なリファレンスやチュートリアルがあります。エラーメッセージに遭遇したら、まずはメッセージをよく読んで何が問題なのかを理解することが重要です。わからない場合は、エラーメッセージで検索すると解決方法が見つかることが多いです。
https://www.raspberrypi.org/ - Raspberry Pi公式サイト
https://www.raspberrypi.org/learning/parent-detector/ - Parent Detector
https://www.ruby-lang.org/ - Ruby公式サイト
https://www.python.org/ - Python公式サイト
http://blog.muchuu.net/post/113402952220/ - nullmailerのセットアップ
http://lchikaamazon.hatenablog.com/ - Raspberry Pi電子工作
http://fritzing.org/home/ - 電子回路図描画アプリ
http://www.nano-editor.org/ - GNU Nano公式サイト
https://github.com/richardghirst/PiBits/tree/master/ServoBlaster - サーボモータ制御ドライバ
http://www.sharpsma.com/Webfm_Send/1208 - 測距センサ
https://projects.raspberrypi.org/en/projects/getting-started-with-minecraft-pi
Getting Started with Minecraft Pi
https://github.com/lumbermill/takachiho/tree/master/raspi/08-motion - 動体検知motionの使い方
https://qiita.com/lumbermill/items/d4b3450e4ffa4fee1783 - RaspbianでOpenJTalkを使う
https://github.com/lumbermill/takachiho/tree/master/raspi/09-signage - キオスク端末化する
https://rubyonrails.org - Ruby on Rails
https://getbootstrap.com - Bootstrap
https://faavo.jp/miyazaki/project/3218 - Faavo宮崎
RaspberryPiで学ぶ電子工作 金丸隆志 BULE BACKS
2015年に執筆した入門書をベースにして、様々な課外授業向けに内容を加筆し続けています。
「CC BY 4.0」に則った再利用・再頒布が可能です。本書に関するお問い合わせ、誤字・脱字等のご指摘は同社問い合わせフォームにお寄せ頂ければ幸いです。著者のSNS等に直接メッセージ頂く形でも問題ありません。一部のコードはGitHubに公開してあります。公開できないものは一切なく、ただただ筆者の怠惰によるものですので、「ここの詳細が欲しい」といったご要望があれば是非お寄せください。
著者: ITO Yosei 伊藤陽生 伊藤製作所・株式会社ランバーミル代表。2003年頃からシステム開発に携わっています。Cから始まりJava、JavaScript、Objective-C、PHP、 Python、SQLなどを扱ってきました。現在は主にRubyを業務に用いています。Linuxサーバの構築だったり、ウェブサイトのデザインをしたり、これらの分野の研修講師として働いていたこともあります。技術の移り変わりの速いこの領域においては、それぞれの経験値そのものよりも、いかに新しい技術にうまくキャッチアップし、チームで共有し、身近な問題の解決に結びつけられるか、という、もう一段抽象的な技能こそが重要なのではないかと考えています。
ここまで目を通してくださりありがとうございます。2015年ごろからRaspberryPi関連のお仕事の機会をくださっているクライアント企業の方々のおかげで私自身が学びを深める機会を持つことができ、このような形でこれまでの経験とノウハウを蓄積することができました。
とはいえ執筆には少なくない時間が必要です。当初はこのテキストをepubやPDFに変換し、オンライン書店(AmazonやApple Books)にて販売していましたが、たびたび大幅な改訂が必要になり、そのメンテナンスが追いついていない現状があります。今後、こうした不整合を避けるためウェブに一本化していく予定です(コピーガードなしのepubも配布しています)。もしこの本の情報が何らかの役に立った、という部分があれば、以下のリンクから応援購入を頂ければ幸いです。今後も継続してアップデートを続ける上で大変励みになります。
ご要望や誤字・誤りの指摘などは(こちらではなく)上記のお問い合わせフォームやSNS等から直接筆者にフィードバックを頂ければ幸いです。
https://buy.stripe.com/bIYbLF7jIdqzaNa146