Dive into Raspberry Pi 2024

はじめに

時は2024年6月、、4回めの序文です。ちょうど10年くらい前に「最高のおもちゃを手に入れた!」と歓喜しつつも、どうやって活用しようかと試行錯誤する過程をそのままテキストに起こしはじめました。Raspberry Piも随分と進化し、ちょっとしたデスクトップPCとしても使えそうなスペックに到達した一方で、消費電力や放熱の問題、価格の上昇などとのトレードオフも発生しています。

今回再編成するにあたり、Raspberry Pi 5に最新のOS(Raspberry Pi OS 64-bit Bookworm)をインストールして検証しています。Bullseye以前のバージョンで実行するには読み替えが必要な箇所があるかもしれません。ご了承ください。また、Raspberry Pi 5で直接検証ができなかった機能についても、参考として残している場合があります。

(2021年版の序文) 運良く3年ごとに書き換えのチャンスが巡ってくるみたいです。初版は2015年、次は2018年にこの前書きを書いています。今回、2021年は、宮崎北高校様向けの「Raspberry Pi Zero W」を使ったセンシング技術体験、及び宮崎商業高校様向けの「Raspberry Piを活用したウェブアプリケーション作成」に向けて改訂と加筆を行っています。

一部の内容は、「RaspberryPiを用いたIoT製品制作(2020年9月)」と重複しています。ご了承ください。今回から「CC BY 4.0」ライセンスを巻末に明記しています。どなたでも自由に再利用・再頒布が可能ですので、気になる内容があれば、是非、授業や社内研修に取り入れてみてください。

(2018年版の序文) この資料(電子書籍)は、2018年に実施したクラウドファンド「宮崎の子供達にプログラミングの楽しさを知ってもらう為の設備を拡充したい!」のリターン品の一つとして用意されたものです。当初は既存のPDFをそのまま配布する予定だったのですが、事前に中身を読み返してみると現状と異なる部分が点在することに気が付き、また(これはオフレコですが)、著者の予想に反してこのリターン品を選ばれる方が多かったことで、これを機に現状に即した内容に書き換えることに致しました。かようにインターネットとコンピュータの世界は進歩が早く、やっと覚えた!と思った技術があっという間に過去のものになる、という現象を繰り返しています。

次々に押し寄せる波に翻弄されるのではなく、次から次に波がやってくることを楽しみにしながら、次の波にはうまく乗ってやろう!と前向きに捉えて行きましょう!

(2015年版の序文) この書籍は、著者が宮崎県高千穂町のコワーキングスペース(452)で3回に渡って開催したプログラミング教室で扱った内容を、Raspberry Piを使った基本的なプログラミング学習の入門書として再構成したものです。RubyにPython、それからScratchやMinecraftなどなど、さらにはちょっとした電子工作まで、短い時間で多岐に渡ってしまいましたが、この本の内容を入り口に興味を持った分野をさらに掘り下げていくきっかけになれば幸いです。

対象読者と前提知識

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

用語と基礎知識

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

RaspberryPi

Raspberry Pi

イギリスで教育用に開発された手のひらサイズの小さなコンピュータ。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」なども本書で扱ってきています。各モデルの大きな違いは処理性能ですが、備えている端子の種類がそれぞれ異なっていますので、必要な周辺機器が使えるかはモデルごとに確認が必要です。

RaspberryPiOS(Raspbian)

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

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

Ruby

Ruby

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

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

Python

Python

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

nano

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

準備と基本操作

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

必要な機器

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

時々必要になるもの:

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

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

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

OSのインストール

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

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

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

GUIを起動する

(通常インストールした場合、GUIが自動的に立ち上がる設定になるため、以下は不要です。)

GUIとはGraphical User Interfaceの略で、WindowsやMacのようにマウスでボタンをクリックしてアプリケーションを起動したり操作したりできる環境のことです。今はGUIが当たり前の環境ですけれど、昔はこの黒い画面だけで様々な事務処理や計算を行っていました(もちろん今でも使われています)。GUIを立ち上げるコマンドは「startx」です。

startx
Desktop

しばらくすると以下のような画面が立ち上がってマウスカーソルが操作できる状態になります。左上の「Menu」ボタンから予めインストールされた様々なアプリを立ち上げてみることができますので、色々と試してみてください。おすすめは、Minecraft Pi Editionです。Minecraftは言わずもがなの世界的に有名なゲームですが、その世界観の一部をすぐに試してみることができます(通常版に比べるとかなり機能は制限されていますが、雰囲気は楽しめます)。また、ウェブブラウザも通常のPCに搭載されているものと遜色のない機能を持っていて多くのサイトを閲覧することができます。

画面の右上に各種の設定アイコンが並んでいますが、この中にネットワーク接続の設定アイコンがあります。Wifiを利用する場合はここからSSIDとパスワードを設定してください。

リモート接続

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

sudo raspi-config
Raspberry Pi Configration

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

本書では、SSH及びVNCを有効にした状態を想定して解説をしています。

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

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

ターミナルの使い方

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

piuser@raspberrypi:~ $

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

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

ls

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

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

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

nanoエディタの使い方

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

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

nano sample.txt

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

nano menu

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

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

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

Pythonの基本

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.

インポート(import)

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

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

import os

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

os.listdir(".")

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

配列

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

a = [1,2,3]

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

a = list(range(1,4))

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

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

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

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

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

a = (1,2,3)

辞書

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

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

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

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

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

del(d['a'])

制御文字を利用する

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

import random, sys, time

color = 91  # 90:gray, 91:red , 92:green, 93:yellow, 94: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')
parser.add_argument('-s','--symbol')

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)

Rubyの基本

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

sudo apt install ruby

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

$ ruby -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [aarch64-linux-gnu]

最新の機能も導入したい場合は、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 == 0
  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の基本

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 ...

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

参考: https://git-scm.com/

実践課題1 - misc

2018年版の冊子に掲載していた課題です。RaspberryPi3とRaspbian(Desktop)の組み合わせが前提のため、最新のRaspberryPiOSでは動作しないものが含まれますが、古いバージョンのRaspberryPiも少ない消費電力が求められる場面などで有用なため、説明としては残しています。

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

libcameraモジュールはrpicamに名称変更されました。今後はrpicamを利用することが推奨されるようです。

rpicam-hello

以下は、raspistillコマンドの説明ですが、rpicam-stillコマンドでも似たような使い方が可能なようです。

Raspberry Piにカメラモジュールが接続されていれば、簡単なコマンド操作で写真や動画を撮ることができます。写真は、前節でも紹介した「raspistill」コマンドを利用します。「-o」というオプションに続けて出力ファイル名(ここではpic2.jpg)を指定しています。

$ raspistill -o pic2.jpg

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

$ raspistill -t 1 -vf -hf -o pic3.jpg

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

$ raspivid -o vid.h264

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

$ raspivid -o vid.h264 -t 60000

保存された動画はh264コーデック(ここは著者も詳しくないので割愛します)に対応したプレイヤがあれば、WindowsやMacでも再生可能です。Raspberry Piで閲覧する場合は、GUIが立ちあがった状態で、以下のコマンドを実行してください。

$ omxplayer vid.h264 -o hdmi

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

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

sudo apt install fswebcam

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

fswebcam ~/Desktop/test.jpg

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

sudo apt install python3-opencv

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

import cv2

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

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

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

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

$ sudo apt install apache2

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

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

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

http://raspberrypi.local/

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

apache2

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

メールサーバを立てる

2024年版追記 nullmailerを使ってお手軽にメールを転送する手順の解説でしたが、現在はGoogle側でapp passwordの提供が終了しているようで、この方法を新規にセットアップすることが出来なくなっています。メールの配送が必要な場合は、なんらかの配信サービス(AWS SESやSendGridなど)を利用することになると思います。nullmailerパッケージ自体はまだ存在しているようなので、以下の説明も参考資料として残しています。

ウェブサーバの次はメールの送受信を行うメールサーバを導入してみましょう。本格的なメールサーバの運用も可能なのですが、これだけでも本が一冊になってしまうくらい複雑なテーマになりますので、ここでは、Gmailという既存のメールサービスを利用してメールの送信のみを行う簡易的なメールサーバを導入します。インストールは先ほど同様、一行でお終いです。

$ sudo apt install nullmailer mailutils

続けて設定ファイルを編集します。

$ sudo nano /etc/nullmailer/remotes

中身は以下のようになります。まず、Gmailのアカウントを一つ取得して頂く必要があります。既存のものを使う手もありますが…セキュリティの観点からあまりお勧めはできません。取得したアカウントの情報でuserとpassの値(xxx@gmail.comとpassword)を書き換えてください。

/etc/nullmailer/remotes

smtp.gmail.com smtp --port=465 --auth-login —-user=xxx@gmail.com --pass=password --ssl

(実際は一行です)

設定を更新したら、メールサーバを再起動します。

$ sudo systemctl restart nullmailer

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

$ echo hi | mail -s 'test mail' xxxx@youremail.com

合わせ技にチャレンジ

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

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

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

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

$ nano take-pictures.rb

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

take-pictures.rb

10.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から009.jpgまで連番の形式で合計10枚画像を保存します。撮れた写真はウェブサーバで公開されています。最後に表示されるURLをブラウザで開いて確認してください(以下は一例です、ホスト名の設定によって異なります)。

http://raspberrypi.local/pictures/

apache2

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

Scratchでプログラミング体験

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

https://scratch.mit.edu/

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

scratch

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

sudo apt install scratch3

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

Sonic PiでBGMを製作

2024.05追記: RaspberriPi5ではイヤホンジャックが廃止されましたが、こちらのアプリはBluethooth接続のスピーカーをそのままでは利用できないようです。いずれは対応するor回避策があるのかもしれませんが、そのままでは利用できなかったことを付記しておきます。

「Sonic Pi」というアプリを使って、Ruby言語で様々な効果音を合成したり音楽を作ったりすることが出来ます。Raspberry Piのアナログ端子にスピーカーを接続している場合は、スピーカーの設定画面で出力先が「Analog」になっていることを確認してください。HDMI端子に繋いだディスプレイ(テレビ)にスピーカが内蔵されている場合はそのままで大丈夫です。

sonicpi

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

23年12月時点、最新のOS(bookworm)では、以下のコマンドが利用できます。または PrtScrキーを押すだけでもスクリーンショットを撮ることができます(DesktopではなくPicturesフォルダに保存されます)。

grim

以下は、以前のOS(buster)向けの解説です。

Scrotは、Linux上でスクリーンショットを撮るコマンドラインツールです。デスクトップやターミナル、特定のウィンドウのスクリーンショットを撮ることができます。下記のコマンドを実行してインストールします。

sudo apt-get install scrot

ターミナルで「scrot」 と入力し、Enterを押します。ツールバー左上の「FileManager」のpiフォルダに、撮影した日時の名前のファイルができます。「-d」の後に任意の秒数を指定して、撮影するタイミングを遅らせる事ができます。

scrot -d 10   # 10秒後に撮影する。

保存先のファイル名を指定することもできます。

scrot /home/pi/pictures/name.png

Scrotには「-d」以外にも様々なオプションがあります。

-s  マウスで画面の中の特定のウィンドウを撮影
-b  ウィンドウ枠を含めて撮る
-u  ウィンドウ枠を含めずに撮る
-p [1~100]  画像の質を指定する。初期値は75

Minecraftで遊ぶ

24.05時点、RaspberryPi 5では正常に起動しないようです。4までは以下の手順で遊ぶことができます。

Minecraftは、世界的に有名なオープンワールドのゲームです。Raspberry Pi向けの無料版では、Pythonのコードを使って物を自動的に生成する事ができます!

Minecraft

2023.11現在、標準のリポジトリ(apt)からは除外されています。githubから直接ダウンロードしてくることで実行が可能です。

https://github.com/MCPI-Revival/minecraft-pi-reborn

以下のように進んでください。

View Documentation -> View Installation -> Click link on “Download Packages here”.

RaspberryPiで利用する場合、 armhf (64bit版の場合、arm64) を選択します。ダウンロード完了後、以下のコマンドを実行して起動します(バージョン番号、アーキテクチャ(armhf,arm64)の部分は適宜、読み替えてください)。

chmod +x minecraft-pi-reborn-client-2.3.9-armhf.AppImage
./minecraft-pi-reborn-client-2.3.9-armhf.AppImage

AppImageを実行するのにFUSEが必要ですとエラーが出ることがあるようです。その場合、以下のコマンドを実行して必要なライブラリを準備してください。

sudo apt install libfuse2

まず、起動したら世界を探検してみましょう。下記のkeyでプレイヤーを動かしてみましょう。主なキー操作は以下の通りです。

w   前進          Space           ジャンプ
a   左に進む        Spaceを素早く2回 飛ぶ / 落ちる
s   後退          Esc         一時停止
d   右に進む        Tab         マウスをリリース
e   倉庫を開く

Pythonのmcpiというライブラリが提供されています。以下のコマンドでインストールします。

pip3 install mcpi

任意のエディタが利用できますが、ThonnyというPython用のエディタを使うと、コーディングから実行・デバッグまでを一つのウィンドウで実行できるので便利です。メニューから「Thonny」を起動して以下のようなプログラムを書いて実行してみましょう。

from mcpi import minecraft

mc = minecraft.Minecraft.create()

mc.postToChat("Hello world")

画面の下の方に「Hello world」と表示されたらPythonからminecraftの世界への接続成功です!

・テレポート 今度は、コードを書いて移動してみましょう。試しに下記のコードを実行してみましょう。

pos = mc.player.getPos()                         
x, y, z = mc.player.getPos()                     
mc.player.setPos(x, y+100, z)                    

一瞬で、頭上の空に飛び出します。

・ブロックを配置する 次にブロックを置いてみます。setBlockという関数に、場所とブロックの種類を指定します。

x, y, z = mc.player.getPos()                     
mc.setBlock(x+1, y, z, 1)                        

1個の”1”のブロック(=石)が、近くに現れます。

・巨大なブロックの塊を作る 大きなブロックの塊も作れます。先ほどの関数を用います。

stone = 1                                            
x, y, z = mc.player.getPos()                         
mc.setBlocks(x+1, y+1, z+1, x+11, y+11, z+11, stone)

一辺が 10 (=11-1) の立方体が出現します。

・歩いた後に花を咲かせる 歩いている間、0.1秒毎に花のブロックを自分のいる場所に置いていきます。

from mcpi.minecraft import Minecraft
from time import sleep

mc = Minecraft.create()

flower = 38

while True:
    x, y, z = mc.player.getPos()
    mc.setBlock(x, y, z, flower)
    sleep(0.1)

・歩いている間、足元のブロックのコードを得る。 足元のブロックのコードを出力するには、getBlockという関数を使います。

while True:
    x, y, z = mc.player.getPos()
    block_beneath = mc.getBlock(x, y-1, z)
    print(block_beneath)

・芝生の上にだけ花を配置する。 setBlockと、getBlockを使って足元のブロックが芝生の時にを配置してみましょう。

grass = 2
flower = 38

while True:
    x, y, z = mc.player.getPos()  # player position (x, y, z)
    block_beneath = mc.get

動体検知機能付きの監視カメラ

以下は、legacyなカメラ接続方法を利用しています。利用は可能ですが、とても重たい上に安定性にも欠ける印象で、できれば前述の新しいカメラライブラリを利用する方法での実装が望ましいです。

Raspi with camera

motionというパッケージを使うと、Raspberry Piのカメラに動体検知機能を持たせることができます。視野の中に動くものを見つけ出しすことが出来ます。何か動くものがあったときだけ写真を撮ったり、動画を保存したりということが可能です。

sudo apt -y install motion

/etc/modules に以下の行を追加します(v4l2のlは小文字のLです)。

bcm2835-v4l2

/etc/default/motion を編集してデーモンを有効化します。

start_motion_daemon=yes

/etc/motion/motion.conf の下記の設定を修正します。

width 640
height 480
event_gap 10
output_pictures best
ffmpeg_output_movies off
snapshot_interval 1800
locate_motion_mode on

修正内容の意味は以下のようになります。他のパラメタも必要に応じて適宜調整してください。

画像サイズを640x480に(320x240)
動体を検知し続ける長さを10秒に(60秒)
検知中に最も動きのあったフレームのみ保存(on:全て保存)
検知中のムービー保管を無効化(有効)
30分に1回スナップショットを保存(保存なし)
動体を検知した範囲を矩形で囲む(囲まない)

設定を完了したらRaspberryPiを再起動します。 /var/lib/motion に画像(jpg)が蓄積され始めたら正常に動作しています。この画像をファイルサーバやウェブサーバ経由で必要に応じて参照できるようにすれば簡易的な監視カメラとしての利用も可能です。

日本語を喋らせてみる

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

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

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

音声合成には関係ないですが、日本語のテストをしやすくするために、フォントとIME(日本語の入力機能)を追加します。

sudo apt install ibus-mozc fonts-takao

辞書と音声ファイルを明示する必要があります。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/

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

TODO: bookwormでの動作未確認です

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

まずは不要なパッケージを消して、必要なパッケージの準備をします。 日本語フォントはnotoを入れていますが、好みで変更してください。

sudo apt update
sudo apt upgrade -y
sudo apt install -y unclutter fonts-noto xdotool

.config/lxsession/LXDE-pi/autostart を下記の内容で書き換えます。 上2行でディスプレイが省エネモードに入らないように設定し、unclutterでマウスカーソルを隠しています。 使用したのはchromiumブラウザのkioskモードです。

@xset s off
@xset -dpms
@unclutter
@chromium-browser --kiosk --incognito https://example.com/path/to/page

crontabを以下のように設定します。この例では1日に1回画面をリロードしています。

1 0 * * * export DISPLAY=":0" && xdotool key F5

すべての設定が終わったら、端末を再起動します。

sudo reboot

chromiumの設定で指定したインターネット上のウェブサイトが全画面表示された状態になって起動します。コンテンツが頻繁に更新されるような場合には、このような使い方が適していますが、固定のコンテンツを繰り返し表示したいような場合には、ローカル(piユーザのホームディレクトリなど)にHTMLのコンテンツを置いてしまって、直接参照するようにしてしまえば、スタンドアロン(=ネットに接続していない状態)でもコンテンツを表示することが可能です。

Gimp, Inkspaceでお絵描き

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

sudo apt install gimp inkscape

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

gimp

ちょっとした文字や図形を描く程度であれば、ほとんど問題なく滑らかに動きます。

inkscape

詳しい使い方に興味のある方は、書籍やウェブを探してみてください。AbobeのIllustratorやPhotoshopほどではありませんが、そこそこの情報が見つかると思います。

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

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

finder

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

sudo apt install samba

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

sudo nano /etc/samba/smb.conf

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

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

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

sudo smbpasswd -a piuser

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

sudo systemctl restart smbd

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

実践課題2 - for Zero W

この章は、GUIの入っていない「Raspberry Pi Zero W」向けの課題になります。

ネットワークに接続する

RaspberryOSではwpa_supplicantというデーモンでWi-fiに接続することができます。このデーモンの設定ファイルを直接編集する方法もありますが、ここではraspi-configを利用する方法を紹介します。まず、ターミナルで以下のようにコマンドを実行します。

sudo raspi-config

以下のようなメニュー画面が立ち上がります。

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
: (省略)

プログラムを自動起動する(crontab)

Linuxにはcrontabというプログラムを決まった時間に起動するための仕組みがあります。 以下のコマンドで起動されるエディタを使って、設定を行います。 初回起動時のみ利用するエディタを聞かれますので、好みのものを選択してください。 ここまで、このテキストを読み進めて来ている方は、nanoがオススメです。

crontab -e

たとえば、毎日15時にhello.pyというPythonのプログラムを起動するには以下のように書きます。

0 15 * * * python3 hello.py

前半の5つの数字(またはアスタリスク)がそれぞれ「分 時 日 月 曜日」となっていて、「*」が指定された場合は、「全て」という意味になります。以下のように書くと、毎月1日の14:30に起動する、という意味になります。また、「#」で始めた行はコメントとして扱われ処理内容に影響しません。メモを残す場合などに利用します。

30 14 1 * * python3 hello.py
# Run only on the first day of each month.

他にも特殊な構文があります。@rebootに続けて書かれたコマンドは、システムが起動した直後に1度だけ呼ばれます。電源を入れたらいつも動かしたいプログラムがある場合は、ここに書いておくと便利です。

@reboot python3 hello.py

実際には、ただ起動するだけだと何が起こっているのか分からなくなることが多いため、以下のようにリダイレクトとパイプを使って(詳細は割愛)、マシンの上に記録(ログ)が残るように運用すると便利です。

@reboot python3 hello.py 2>&1 | logger -p cron.info -t "hello"

ログは /var/log/syslog というファイルに書き出されています。上記のように hello というタグをつけた場合、以下のように確認することが出来ます。

sudo grep hello /var/log/syslog

現在の設定を確認するには、「-e」の代わりに「-l」オプションを使用します。

crontab -l

「-r」オプションを指定すると、設定が全て消去されます!

crontab -r  # danger!!

センサーの値を取得する(envirophat)

以下のスクリプトでは「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の基本」で学ぶ内容と組み合わせて取り組んでみてください。

データをサーバに送信する(iot.lmlab.net)

ここでは、インターネットを介してセンサーのデータを収集するための方法を試してみます。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 addrpingなどのコマンドを使ってネットワークへの接続状態を確認してください。名前解決に問題がある場合、端末やサーバを再起動することで改善する場合があります。

鍵の登録が出来たら、以下のコマンドで接続が可能かを確認します。サーバの名前(enviro-host)が表示されたら正常です。

ssh enviro-host.local hostname

データをサーバに転送するにはrsyncというコマンドを利用します。

rsync -avz enviro-logger/data/*.csv enviro-host.local:enviro-logger/data/

これもmain.pyと同じようにcronに登録してしまいましょう。 こちらは、main.pyのintervalとのバランスを見て間隔を決めてください。以下は10分おきに転送を起動する例です。

*/10 * * * * rsync -avz enviro-logger/data/*.csv enviro-host.local:enviro-logger/data/

データの蓄積が出来たら、続いて、統計データを作成するためのスクリプトを用意します。こちらは、同じディレクトリの中にstat.pyという名前の別のスクリプトに記述します。

cd enviro-logger
nano stat.py

保存するファイル名はenviro-logger/data/(name)-stat.csvとします(日付の代わりにstatという固定の名前を入れる格好です)。以下のような流れで処理を実行します。

  1. コマンドライン引数(-n)の名前を使って、保存するファイル名を決める
  2. 既に保存されている日別のデータファイルを一つずつ読み込み
  3. さらにファイルごとに1行ずつ値を読み込み
  4. 最低気温、最高気温、平均気温、最低照度、最高照度、平均照度を求めます
  5. 結果をコンマ区切りの1行にまとめます
  6. これらを保存対象のファイル(name-stat.csv)に保存します

全体のスクリプトは以下のようになります。

#! /usr/bin/env python

import argparse, os, sys

def stat(fn):
    # 日単位の、気温、照度の最低、最高、平均
    # date,(temp)min,max,avg,(light)min,max,avg
    # ex. yyyy-mm-dd,3,17,10,100,1500,500
    results = ["",99.99,0,0,9999,0,0]
    n = 0
    with open(fn, mode='r') as f:
        for line in f:
            row = line.strip().split(",")
            results[0] = row[0] # date
            temp = float(row[2])
            light = int(row[3])
            if results[1] > temp:
                results[1] = temp
            if results[2] < temp:
                results[2] = temp
            results[3] += temp
            if results[4] > light:
                results[4] = light
            if results[5] < light:
                results[5] = light
            results[6] += light
            n += 1
    results[3] = round(results[3] / n,2)
    results[6] = round(results[6] / n,2)
    return ",".join(list(map(str,results)))

parser = argparse.ArgumentParser(description='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を利用しますので、インターネットに繋がっている必要があります。

Webview by Sinatra

ファイル構造は以下のようになります。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()

実践課題3 - with GPIO

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用、というのも覚えておいても良いかもしれません。

(2024年版の追加) 2021年版まではRPi.GPIOというライブラリを使っていましたが、こちらがRaspberry 5では使えなくなってしまいました。個人的に別のプロジェクトで利用していたpigpioというライブラリも5に対応しておらず、(githubのやりとりを読む限り)対応の目処もたっていなさそうです。ということで、gpiozeroというライブラリを使って作例を書き直していますが、RPi.GPIOも旧型のRaspberryPiでは使えるため、引き続き掲載しています。

GPIOへの出力(LED点滅)

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

GPIOへの出力

この回路を動かす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()

以下のコマンドで実行します。GPIOの操作は管理ユーザ(root)の権限で行う必要があるため、「sudo」に続けてPythonを起動します。

sudo python gpio-out.py

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

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

次は、逆に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」をタイプしてください。

GPIOへの入力

以下は、ボタンが押された際に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」端子をそれぞれ結線してください。

PIRモーションセンサ

まずは、この回路が正しく動くか検証するプログラムを書きましょう。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!

センサの下部に二つ並んだ黄色いツマミで、センサの感度と反応時間を調整します。プログラムを動かしたまま、サイズの合ったプラスドライバでツマミを動かし、思い通りの反応をする位置を探してください。ほんのちょっと動かしただけでも、結果が大きく変わることもありますので、ここは根気よく試行が必要です。

PIR

調整が終わったら、「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(17)

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秒間 ずつ待機しています。

参考URL: http://bufferoverruns.blogspot.jp/2016/08/raspberry-pisg-90.html

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

ここまでの技術の応用ではありますが、測距センサを使ったプログラムの例も紹介します。写真の右側に写っているのが今回使用しているセンサ(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」は設置位置と向きに応じて適宜調整してください。

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

(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)

https://gpiozero.readthedocs.io/en/stable/recipes.html#full-color-led

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

(GPIO Zeroの基本レシピからの抜粋です)

RaspberryPiではアナログ値を直接読み取る機能がないため、ADコンバータMCP3208を利用します。これまでの参考書ではSPI経由でデジタル値を取得するところから自前で実装する必要があったため、どうしても取っ付きにくい印象がありましたが、gpiozeroでは、様々なADコンバータに直接対応したクラスがあらかじめ用意されており、これを使って簡単に値を取得することができます。

MCP3208

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

from gpiozero import MCP3208
from time import sleep

while True:
  pot = MCP3208(0)
  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

https://gpiozero.readthedocs.io/en/stable/api_spi.html#gpiozero.MCP3208 https://gpiozero.readthedocs.io/en/stable/recipes.html#potentiometer

TODO 照度センサー、温度センサー

モータードライバの利用

TODO DRV8835

LCDディスプレイ

TODO ADT7410

実践課題4 - motion

最新のRaspberryPi(Bookworm)では、ここで利用しているmotionパッケージがそのままでは動作しなくなっています。 24.07.30

動体検知カメラが記録した画像をブラウザ経由で閲覧、統計を見ることが出来るアプリを開発します。 以下の項目を予め理解していることを、前提として記載しています(HTMLの基本以外は本書内にも解説があります)。

以下のような画面構成を想定します。左上のMotion、Streamのリンクは動体検知を実現するmotionのコントロール画面と動画のストリーム(リアルタイムの映像)を参照するために付けました。続けて、「現在保管されている画像の枚数」「保管場所」「(ページを表示した時点の)現在時刻」が表示されています。右端のReloadは画面を再読込するためのリンクです。

緑色の棒グラフには時間帯別に撮影された写真の枚数を(最大7日間)表示しています。その下の「Motions」が画面の中に動くものを検知した際に撮影された写真、「Snapshots」が1時間ごとに定期的に撮影された写真です。グラフも含めていずれも横にスクロールが可能で、実際には下図に表示されているより多くの情報が掲載されています。

この課題で作るウェブアプリ

動体検知カメラのセットアップ

「動体検知機能付きの監視カメラ」の節を参考にして、motion パッケージをインストールします。 /etc/motion/motion.conf の設定は以下のように調整してください。

webcontrol_localhost off
stream_localhost off
snapshot_interval 3600   # 1時間に1回スナップショットを保存
output_pictures best     # 検知中に最も動きのあったフレームのみ保存
ffmpeg_output_movies off # 検知中のムービー保管を無効化
locate_motion_mode on    # 動体を検知した範囲を矩形で囲む

設定完了後、ブラウザで以下のURLにアクセスして現在のカメラ越しの状況を確認することができます。

webcontrol: http://localhost:8080
stream: http://localhost:8081

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

ウェブアプリケーションと一言で言っても、実は数多くの実装方法が存在します。今回は、シンプルなアプリを作るのに便利なSinatraというフレームワークを利用します。Rubyがインストールされていれば以下のコマンドでインストールが可能です。

sudo gem install sinatra -N

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

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

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

require 'sinatra'

get '/' do
  erb :index
end

続けて、main.rbから呼び出される 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>Hello, world!</title>
  </head>
  <body>
    <h1>Hello, world!</h1>
    <p><%= Time.now %></p>
  </body>
</html>

2つのファイルが用意できたら、以下のコマンドでSinatraのWebサーバを起動します。

ruby main.rb

開発用では、自分自身のホストからしか起動中のページを見ることができません。他のホストからもページを見えるようにするには、APP_ENVという環境変数を設定します。

APP_ENV=production ruby motion/main.rb

ブラウザから http://localhost:4567 にアクセスして、「Hello, world!」と書かれたページが見えたら起動成功しています。(アクセスするURLは環境によって異なります。sinatraの出力を読んで確認してください。

今度は、写真を読み込めるようにします。main.rbのrequire 'sinatra'の下に、以下の一行を追加して、ウェブアプリ側から撮影された写真が見えるように設定します。

set :public_folder, '/var/lib/motion'

以下のように、erbのコードを書いて、画像をページ内に表示することが出来るようになります。

<%
fnames = Dir.children(settings.public_folder)
fnames.each do |f| %>
  <img src="<%= f %>" alt=""/>
<% end %>

これを使って撮影画像を任意のレイアウトで表示することが出来るようになりますが、今回はもうひと手間かけて、JavaScriptを経由して別のページで生成された写真とグラフを取得するように作り込んでみます。

main.rbの最下部に以下の設定を追記します。

get '/motions' do
  erb :motions
end

あわせて、views/motions.erbというファイルを新規作成し、先程の画像を表示するerbコードをその中に記載してください。サーバを再起動して、 http://localhost:4567/motions に撮影済み画像が表示されていれば成功です。

views/index.erbのbodyタグの最後尾に以下のようなコードを追加します。<script>で囲まれた部分はJavaScriptという言語で記述されています。このコードを差し込むと、再びトップページ(最初のmotionsが付かない方のURL http://localhost:4567)で画像が表示されるようになります。

<div id="motions"></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");
</script>

ちょっと(かなり?)大雑把な説明ではありますが、これらの基本部分を元に、最初の画面にあったような監視カメラの機能を実装していきましょう。HTML・CSS、Ruby、JavaScriptの知識が必要ですが、インターネット上にリファレンスや豊富なサンプルがありますので、それらを適宜参考にしてください。この後に実装例が掲載されていますが、そちらはなるべく見ずに、最初の画面を自分で作るならどうするか?どんな知識が必要か?を一つ一つ考えながら、なるべく手探りで進めてください。

特に、時間帯別のグラフ描画は難易度の高い部分です。motionによって保存される画像ファイル名のパターンをよく観察し、どの部分を抜き出したら「保存された日時」が取り出せるかをまず考える必要があります。

"01-202110101234-snapshot.jpg".split("-")[1].to_i / 10000

沢山の関数が、「.(ドット)」でつながれて一見するととても複雑ですが、ひとつずつ分解して、どんなどうさをするのかirbなどのツールを使って確認しながら理解を深め、実際のコードに応用していきましょう。エラーメッセージも英語ではありますが、重要な情報が含まれています。まずはじっくり読んでみて何を言われているのかを理解するように努めてみてください。

実装例

main.rb

require 'sinatra'

set :public_folder, __dir__ + '/var_lib_motion'
configure :production do
  # APP_ENV=production
  set :public_folder, '/var/lib/motion'
end

get '/' do
  erb :index
end

get '/motions' do
  erb :motions
end

get '/activities' do
  erb :activities
end

views/index.erb: メインページのコードです。写真の枚数や現在時刻などの情報はここで直接表示しています。また、画面のレイアウトに関する設定も主にここにあります。グラフと写真はJavaScriptを使って、別のページの結果を埋め込んでいます。

<html>
<head>
  <title>Motion</title>
  <style>
    body{
      font-family: Tahoma, sans-serif;
    }
    div#activities, div#motions, div#snapshots{
      display: flex;
      overflow: scroll;
    }
    div#motions img, div#snapshots img{
      max-width: 240px;
    }
    h2{
      clear: both;
    }
  </style>
</head>
<body>
  <a href="http://<%= request.host %>:8080">Motion</a>
  <a href="http://<%= request.host %>:8081">Stream</a>
  <small><code><%= Dir.children(settings.public_folder).count %> pictures in <%= settings.public_folder %></code></small>
  <span><%= Time.now %> <a href="/">Reload</a></span>
<div id="activities" style="height: 120px"></div>
<h2>Motions</h2>
<div id="motions"></div>
<h2>Snapshots</h2>
<div id="snapshots"></div>

<p>To get all pictures:</p>
<pre>
rsync -avz pi@<%= request.host %>:/var/lib/motion/ motion/
</pre>
<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();
}

function load_motions(id,offset){
  var path = "/motions?offset="+offset+"&id="+id;
  load(id,path);
}
load_motions("motions",0);
load_motions("snapshots",0);
load("activities","/activities");
</script>
</body>
</html>

views/motions.rb: 撮影された画像を表示する部分です。名前から判別して、動体検知された写真(数字のみ)と、定期撮影された写真(snapshot)のそれぞれを表示するように動作変更をすることが可能です。reverse[0,10]で「最新の10枚」のみを表示しています。

<%
sp = params[:id] == "snapshots"
fnames = Dir.children(settings.public_folder)
  .select {|f| sp ? f.include?("snapshot") : !f.include?("snapshot") }
  .sort_by {|f| f.split("-")[1].to_i }.reverse[0,10]
%>
<% fnames.each do |f| %>
  <div>
    <a href=""><img src="<%= f %>" alt=""/></a>
    <small><%= f %></small>
  </div>
<% end %>

views/activities.erb: こちらはグラフの表示部分です。1時間単位の撮影枚数を取得する部分のコードがちょっと複雑です。7日分のループを回して横長のグラフを書くようにしています。このグラフの各要素をクリックすると、その時間帯の写真が見える、などのように拡張するのも面白そうです。

<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>
<%
freqencies = Dir.children(settings.public_folder)
  .select {|f| !f.include?("snapshot") }
  .map {|f| f.split("-")[1].to_i / 10000 }
  .group_by(&:itself)
  .map{|k,v| [k,v.count] }.to_h
h = (120 - 1) / freqencies.values.max

t = Time.now
h_offset = t.hour
(24 * 7).times do
  i = t.strftime("%Y%m%d%H").to_i
%>
  <td>
    <div class="activities-box" style="height: <%= freqencies[i].to_i*h+1 %>px"></div>
  </td>
<%
  t -= 3600
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サーバを立ち上げていましたが、運用中は電源を入れたらそのまま使えるようにした方が便利です。cronという自動実行の仕組みを使って設定します。

crontab -e

立ち上がってきたエディタ(最初は選択画面が表示されます)を使って以下のような行を設定します。

@reboot    APP_ENV=production ruby webapp/main.rb 2>&1 | logger -p cron.info -t "motion"

@reboot」は「起動後に実行する」という意味で、その後ろに実行したいコマンドを記述します。「2>&1 |」はリダイレクトとパイプと呼ばれる機能です。これを使って、このプログラムからの出力(エラーも含む)のログ(処理内容の記録)を保管するように設定しています。

参考URL

実践課題5 - webapp

この章ではRaspberryPiをウェブアプリケーションサーバとして利用する方法を解説します。ここで利用するフレームワークは[Ruby on Rails]です。拙著、Ruby on Railsで作るウェブアプリケーション(Rails7対応版)の内容に近いものをRaspberryPi環境向けに書き変えた構成になっています。

まずは必要なパッケージを導入します(postgresqlは後半の応用課題で使います)。

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

rbenvはRubyの実行環境を管理するツールです。これを使ってrubyをインストールします。

rbenv install 3.1.2
rbenv global 3.1.2
rbenv init  # ここに出てくる指示に従って設定ファイルを書き換えます

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

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

(以下3行のコマンドは、必要な方のみ参考にしてください。スキップしても構いません) 本当は直接Rubyのパッケージをインストールしたかったのですが、後述するライブラリ(gem)の幾つかが、直接インストールされることを想定しておらずエラーが多発したため、この手順を踏むことにしています。RaspberryPiOSの提供するrbenvはかなり古いバージョンにしか対応していません。最新の環境で試したい方は、以下の手順で、最新版もインストール対象に加えることができます。

sudo apt remove ruby-build
mkdir -p "$(rbenv root)"/plugins
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

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

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

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

ruby -v   # ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [aarch64-linux]
rails -v  # Rails 7.2.0
psql -V   # psql (PostgreSQL) 15.6 (Debian 15.6-0+deb12u1)

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

rails new rails01-rooms --minimal
cd rails01-rooms

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

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

bundle exec rails server

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

http://localhost:3000

rails

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

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

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

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

bundle exec rails s -b 0.0.0.0

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

足場づくり - scaffold

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

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

bundle exec rails g scaffold room name occupied:boolean

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

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

bundle exec rails db:create db:migrate

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

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

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

rails

窓口づくり - api

次に、入退室の状態変更を外部から受け付けるためのAPI(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 というホスト名でアクセス可能ですが、それ以外の場合、設定したホスト名.localと読み替えてください。

curl http://raspi5.local:3000/rooms/update-occupation -XGET -d 'name=Room A&occupied=true'

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

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

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

飾りつけ - bootstrap

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

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" %>
bootstrap button

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

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

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

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

最後に、show.html.erb のボタンにもスタイルをつけてみましょう。mt-2 というクラスは要素の余白をコントロールするためのもので、この場合、 margin t op を5段階中 2 番目のサイズ確保するという意味になります。他にも pb-5 = padding 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" %>
show page with bootstrap style

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

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

https://getbootstrap.com/docs/5.2/getting-started/introduction/

ちょっと今風に - actioncable

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

JavaScriptの導入が必要です。前節では、 --minimal オプションをつけて最小構成でプロジェクトを作成しましたが、今度はそれを指定しません。また、データベースエンジンをPostgreSQLに変更しています。何も指定しない場合はSQLiteというファイルベースのデータベースが作成されます。

rails new rails02-with-cable --database=postgresql
cd rails02-with-cable

今回はデータベースエンジンにPostgreSQLを使うため、 config/database.yml を編集し、default:セクションに以下を追加します。これでデータベースとの接続ができるようになります。

  username: piuser
  password: secret

この設定に合わせて、データベースの設定も調整しておきます。

sudo su - postgres
createuser piuser -d
psql postgres -c "alter user piuser password 'secret'"

まずは、前節と同じ手順で rooms の scaffold を作成してください。ここまで準備できている前提で進めます。

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

rails g channel notification

config/cable.yml を編集します。双方向通信のアダプタにpostgresqlを使用します。

development:
  adapter: postgresql

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

stream_from "notification_channel"

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

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

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

location.reload();

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

rails action cable

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

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

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

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

[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=/home/piuser/.rbenv/shims/puma -C /var/www/myapp/current/config/puma.rb

Restart=always

[Install]
WantedBy=multi-user.target

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

systemctl daemon-reload
systemctl enable puma
systemctl start puma

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

sudo systemctl restart puma

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

発展課題例

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

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

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

実践課題6 - cv,yolo

この章では、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(物体検出、ポーズ推定)を試してみる

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

import cv2

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

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

    cam.release()
    cv2.destroyAllWindows()

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

yolo

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

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

import cv2

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

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

    cam.release()
    cv2.destroyAllWindows()
yolo

OpenCVで画像を処理する

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

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

import numpy
import cv2

SRCDIR = "samples"
DSTDIR = "results"

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

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

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

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

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

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

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

dog

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)
result of blur

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

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

shapes
import cv2
import common
import numpy

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

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

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

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

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))
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(img,'OpenCV',(10,500),font,4,(255,255,255),2,cv2.LINE_AA)

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

opencv4-colors.py : 色を扱うサンプルです。

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)
result of colors

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)
result of pixel

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)
result of roi

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
result of split

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
result of thresold

発展課題例

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

補遺

参考資料

更新履歴

本書について

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

payment link by Stripe