2023年3月29日水曜日

書籍で用いたコマンドおよび補足情報(Pi Zero~Pi 4までの古い情報)

より新しい情報

Raspberry Pi 5 が海外で発表され、それに対応した OS Bookworm がリリースされるに伴い、本ページの内容は古くなっています。下記の新しいページをご覧ください。 以下は、それ以前の古い情報を残しておいたものです。

サンプルプログラムと回路配線図について

サンプルプログラム、回路配線図、応用PDFは下記のリンクからダウンロードしてください。 回路配線図の PDF と応用PDF は Raspberry Pi 上のブラウザでも見られますが、ブラウザ上の「↓」(ダウンロード)ボタンでダウンロードし、ファイルマネージャーで PDF ファイルを右クリックし「アプリケーションで開く」→「アクセサリ」→「ドキュメントビューア」などで開いてもよいでしょう。
「選択したアプリケーションをこのファイルタイプのデフォルトのアクションとする」にチェックを入れればそのアプリケーションで開くのがデフォルト動作となります。

本ページで表示するコマンドの利用方法

本ページには、本書の演習を実行するために必要なコマンドを全て記し、さらに、本書への補足情報を記していきます。コマンドを本ページからコピーして実行することにより、書き写すことによるミスの恐れがなく確実に実行できます。

本ページのコマンドをコピーにより活用するためには、まずRaspberry Pi上のブラウザ(Chromium)で本ページを開きます。

そして、コピーしたいコマンドをマウスでなぞり色を反転してください。その状態からそのコマンドをコピーするには下記の二つの方法のうちどちらかを実行してください。
  • 色を反転した領域をマウスで右クリックして「コピー」を選択する
  • キーボードの「Ctrl」キーを押しながら「c」キーを押す(Ctrl-c)
そして、コピーされたコマンドをターミナルソフトウェアに貼り付けるには、下記の三つの方法のうちどれかを実行してください。
  • LXTerminalのメニューから「編集」→「貼り付け」を選択する
  • LXTerminal上でキーボードの「Ctrl」キーと「Shift」キーを押しながら「v」キーを押す(Ctrl+Shift+v)
  • LXTerminal上でマウスのホイールを押し込む(ただし、この方法は正確には「コピーされた文字を貼り付ける」のではなく「マウスで色が反転された文字を貼り付ける」という動作になります)
いずれかの方法でLXTerminalにコマンドが貼り付けられたら、そのままキーボードの「Enter」キーを押せばコマンドが実行されます。

なお、複数のコマンドが複数行にわたって連続して書かれている場合、コピー、貼り付け、実行はコマンド一つごとに行ってください。

2章

p.29: OSインストール法の最新版について

OSである Raspberry Pi OS のインストールから設定の流れは、書籍執筆時から少しずつ変更されております。
最新のインストールおよび設定方法は、 「Raspberry Piではじめる機械学習 補足情報」内にあるRaspberry PiへのOSのインストール方法をご覧ください。本書2章と同等の内容をアップデートされた状態で見ることができますので、参考にしてください。

p.33, microSDカードの容量について

書籍では、容量が16GB以上のmicroSDカードを推奨しました。しかし、2023年10月にリリースされたOSでは、16GBでは容量が足りなくなることを確認しています。容量が32GB以上のmicroSDカードを用意して下さい。

p.49, Raspberry Pi Zero W 系の機種に対する周辺機器の取り付け

Raspberry Pi Zero W 系の機種へ周辺機器を取り付けて電源を入れる方法は以下のページをご覧ください。

3章

p.65, 秋月電子通商のパーツセット

本書用の秋月電子通商のパーツセットは、以下のリンク先で購入できます。
下記のように、書籍とパーツのセットもあります。

5章

p.128, Bookworm (Raspberry Pi OS 2023-10-10 以降) および Bullseye (Raspberry Pi OS 2021-10-30 以降) でのカメラの利用について

Raspberry Pi OS 2023-10-10 からはじまったバージョン Bookworm および
Raspberry Pi OS 2021-10-30 からはじまったバージョン Bullseye では
カメラモジュールの利用方法に大きな変更が加えられ、libcamera というライブラリを用いるようになりました。 これは、本書でカメラを用いるプログラムは、そのままでは動かなくなるということを意味します。

ただし、Bullseye では「Legacy Camera」という、これまでと互換性のあるカメラの利用方法が可能になっています。この Lecgacy Camera を用いれば本書でカメラを用いるプログラムはそのまま動作しますが、 残念ながら Bookworm では Legacy Camera は削除されてしまいました。

以上を踏まえて現状をカメラの利用方法について整理すると、以下のようになります。将来を見据えると、最新の OS で「本書の演習を picamera2 (libcamera) で実行する方法」を使っていただくのが良いのではないかと思います。

32-bit 版 および 64-bit 版 Bookworm および Bullseye本書の演習を picamera2 (libcamera) で実行する方法」で配布しているサンプルファイルを利用する。なお、2023年1月に発売された Camera Module v.3 はこの手法でしか使えません。
32-bit 版 BullseyeLegacy Cameraモードを有効にした上で、本書付属のサンプルファイルをそのまま用いる。これが一番簡単。
64-bit 版 BullseyeLegacy Cameraモードを有効にした上で、「本書の演習をウェブカメラで実行する方法」で配布しているサンプルファイルを利用する

さて、上記の方法のうち、Bullseye で Legacy Camera モードを有効にする方法を用いる場合は、以下の方法で「Legacy Camera」のサポートを有効にする必要があります(デフォルトは無効です)。

ターミナルを開いて以下の手順に従うことで、raspi-config により Legacy Camera を有効にします。
  1. 「 sudo raspi-config 」を実行することで raspi-config を起動
  2. キーボードの「↓」キーを2回押し、「Interface Options」を選択してから「Enter」キーを押す
  3. 「I1 Legacy Camera Enable/Disable」があらかじめ選択されているので、「Enter」キーを押す
  4. 「Would you like to enable legacy camera support?」と聞かれるので「←」を一回押すことで「はい」を選択し、「Enter」キーを押す
  5. 「非推奨 (deprecated) であり将来サポートされない」という趣旨のことを言われるが、「了解」が選択された状態で「Enter」キーを押す
  6. raspi-configを起動した状態の画面に戻るので、「TAB」キーを二回クリックし、「Finish」を選択した状態で「Enter」キーを押す
  7. 再起動を促されるので、「はい」が選択された状態で「Enter」キーを押す
以上で、Legacy Camera モードが有効になりますので、あとは上の表に従ってプログラムを利用してください。

p.129, Raspberry Pi Zero WH にカメラモジュールを取り付ける方法

Raspberry Pi Zero WH にカメラモジュールを取り付ける方法以下のページをご覧ください。

p.132, コマンドプロンプトの表記

p.132ではコマンドプロンプトの表記として以下を紹介しました。
pi@raspbberrypi:~ $
ここに見える「pi」はユーザー名を表しており、ユーザー名「pi」は2022年4月以前の古いOSで用いられていたデフォルトのユーザー名です。最新のOSを用いている方ならば、「pi」の部分に自分で決めたユーザー名が表示されているでしょう。

p.132, mplayer のインスト―ルコマンド

以下のコマンドは、インストール可能なソフトウェアのリストを更新します。
sudo apt update
以下のコマンドで、mplayerをインストールします。
sudo apt install mplayer
途中で「続行しますか?」や「検証なしにこれらのパッケージをインストールしますか?」と質問された場合、それぞれで[y]をタイプしたあと[Enter]キーを押して続行しましょう。

p.133, mplayer の実行コマンド

プログラムの存在するディレクトリ(フォルダ)で以下のコマンドを実行すると、test.mp3 というサンプル音声を再生することができます。音が鳴らない場合は引き続き以下もお読みください。
mplayer test.mp3
なお、サンプルプログラムを bluebacks ディレクトリ(フォルダ)に保存した場合は、上記コマンドを実行する前に以下のコマンドを実行して bluebacks ディレクトリ内に移動する必要があります。
cd bluebacks

p.134, 音声の出力先を変更する方法

音声を HDMI ケーブル経由ではなく、Raspberry Pi の基板上のイヤフォンジャックから聞きたい場合、デフォルトでは音が鳴らないことがあります。 イヤフォンジャックから鳴らすための方法は、OSのバージョンにより異なります。

<2023-10-10版以降の Raspberry Pi OS をご利用の場合(Bookworm)>

もし、デフォルトでピンジャックから音が出ていない場合、以下の指示に従ってください。なお、raspi-config コマンドを実行中は、キーボードの Esc キーが「戻る」に対応しますので、困ったら Esc キーを何度か押してみると良いでしょう。
  1. ターミナルで「 sudo raspi-config 」コマンドを実行し、設定画面を開く
  2. キーボードの「↓」キーを五回押し、「6 Advanced Options」にフォーカスを合わせる
  3. キーボードの「Enter」キーを押し、「6 Advanced Options」に入る
  4. キーボードの「↓」キーを六回押し、「A7 Audio Config」にフォーカスを合わせる
  5. キーボードの「Enter」キーを押し、「A7 Audio Config」に入る
  6. キーボードの「上」キーを一回押し、「1 PulseAudio」にフォーカスを合わせる
  7. キーボードの「Enter」キーを押し、「1 PulseAudio」を選択する
  8. キーボードの「Enter」キーを押し、「了解」を選択する
  9. キーボードの「TAB」キー二回を押し、「Finish」にフォーカスを合わせる
  10. キーボードの「Enter」キーを押すと、再起動され設定が有効になる


<2020-12-02版およびそれ以降の Raspberry Pi OS をご利用の場合(Bullseye)>

2020-12-02版およびそれ以降の Raspberry Pi OS をご利用で、なおかつイヤフォンジャックから音が鳴らない場合、raspi-configというコマンドで音声の出力先をイヤフォンジャックに切り替えます。 以下の手順に従ってください。
  1. ターミナルで「 sudo raspi-config 」コマンドを実行し、設定画面を開く
  2. キーボードの「Enter」キーを押し、「1 System Options」に入る
  3. キーボードの「↓」キーを一回押し、「S2 Audio」にフォーカスを合わせる
  4. キーボードの「Enter」キーを押し、「S2 Audio」の設定画面に入る
  5. キーボードの「↓」キーを一回押し、「1 Headphones」にフォーカスを合わせる
  6. キーボードの「Enter」キーを押し、「1 Headphones」を選択する
  7. キーボードの「TAB」キー二回を押し、「Finish」にフォーカスを合わせる
  8. キーボードの「Enter」キーを押し、raspi-config の設定画面を終了する

以上で、下記コマンドで
mplayer test.mp3
イヤフォンジャックから音声が出るようになります。

<2020-5-27版および2020-8-20版の Raspberry Pi OS をご利用の場合>

次は、2020-5-27版および2020-8-20版の Raspberry Pi OS をご利用の場合で、なおかつイヤフォンジャックから音が鳴らない場合です。 音声を再生するコマンドを以下に変えるとイヤフォンジャックから音が鳴ります。 「-ao alsa:device=hw=1,0」が「イヤフォンジャックからの再生」を意味します。「-ao alsa:device=hw=0,0」なら「HDMIからの再生」です。
mplayer -ao alsa:device=hw=1,0 test.mp3

<2020年2月までの Raspbian をご利用の場合>

2020年2月までの Raspbian をご利用の場合、ターミナルで一度以下のコマンドを実行すると、以後「mplayer test.mp3」コマンドでイヤフォンジャックから音が鳴るようになります。
amixer cset numid=3 1
なお、末尾の「1」がイヤフォンジャックからの音声出力を意味し、「0」なら自動認識、「2」ならHDMIからの音声出力を意味します。

p.134, 05-05-sw-mp3.pyをイヤフォンジャックからの音声出力で実行する

プログラム 05-05-sw-mp3.py をイヤフォンジャックからの音声出力で実行したい場合の補足です。 やはり、OSのバージョンにより方法が異なります。

<2020-12-02版およびそれ以降の Raspberry Pi OS、または2020年2月までの Raspbian をご利用の場合>

2020-12-02版およびそれ以降の Raspberry Pi OS、または2020年2月までの Raspbian をご利用の場合、上で行った「イヤフォンジャックから音を鳴らす設定」を行っていれば、 デフォルトのままの 05-05-sw-mp3.py で音を鳴らすことができます。「デフォルトのままの 05-05-sw-mp3.py 」とは、 12、13行目が以下の状態になっているものです。
            args = ['mplayer', 'test.mp3']
            #args = ['mplayer', '-ao', 'alsa:device=hw=1,0', 'test.mp3']

<2020-5-27版および2020-8-20版の Raspberry Pi OS をご利用の場合>

2020-5-27版および2020-8-20版の Raspberry Pi OS をご利用の場合、プログラム 05-05-sw-mp3.py に対して以下を実行してください。

まず、Thonny でプログラムを開くと、12、13行目に以下の行が見つかります。
            args = ['mplayer', 'test.mp3']
            #args = ['mplayer', '-ao', 'alsa:device=hw=1,0', 'test.mp3']
一つ目の行が実際に実行される行であり、二つ目の行は「#」があるので無効な行です。これを、以下のように「#」の有無が逆になるよう編集してファイルを保存してください。
            #args = ['mplayer', 'test.mp3']
            args = ['mplayer', '-ao', 'alsa:device=hw=1,0', 'test.mp3']
以上により、2020-5-27版および2020-8-20版の Raspberry Pi OS で 05-05-sw-mp3.py をイヤフォンジャックからの音声出力で実行できるようになります。

6章

p.144:半固定抵抗の回転の向きについて

本書では、半固定抵抗として秋月電子通商の「半固定ボリューム 10kΩ」を用いて解説をしています(パーツセットに含まれるものです)。

半固定抵抗は、製品の種類によって、AD変換により得られた値が大きくなる回転の向きが異なることがあります。

そのような場合、下図のように、3.3VとGNDへの接続を逆にすると良いでしょう。

7章

p.165, 接続されている I2C デバイスのアドレスを表示するコマンド

接続されている I2C デバイスのアドレスを表示するコマンドは以下の通りです。
i2cdetect -y 1

p.178, LCD へ文字を表示するプログラムの実行コマンド

ターミナルから 07-02-LCD.py を実行するコマンドは以下の通りです。付録C.3「タブによる補完」を学ぶと、ターミナルでのプログラムの実行が格段に楽になりますので興味のある方は参考にしてください。
python3 07-02-LCD.py
なお、サンプルプログラムを bluebacks ディレクトリに保存した場合は、上記コマンドを実行する前に以下のコマンドを実行して bluebacks ディレクトリ内に移動する必要があります。
cd bluebacks

p.179, LCD へ文字を表示するプログラムをコマンドライン引数つきで実行するコマンド

python3 07-02-LCD.py 'test'

8章

p.203, 用いるモータードライバ

本書では「DRV8835使用ステッピング&DCモータドライバモジュール」というモータードライバを用います。 本書のパーツセットを購入する際、本書の旧版用のパーツセットを購入してしまうと、 TA7291P というモータードライバが同梱されています。手元にこのモータードライバしかないという場合、 「旧版との違いについて」ページにある TA7291P 用の回路の配線図を参考にしてください。

なお、「DRV8835使用ステッピング&DCモータドライバモジュール」が品切れの場合でも、 このモータードライバが同梱された本書のパーツセットは入手可能なことがありますのでチェックしてみてください。

p.217, pigpiod の開始・停止・再起動コマンド

pigpiod の開始・停止・再起動コマンドはそれぞれ以下の通りですので、必要なコマンドをコピーしてご利用ください。
sudo service pigpiod start
sudo service pigpiod stop
sudo service pigpiod restart

9章

p.228, WebIOPi のインストールコマンド

WebIOPi のインストールコマンドは下記の通りです。全てのコマンドが長いので、一つずつ順番にコピーして確実に実行していきましょう。なおコピーの際、先頭の「(1)」、「(2)」などの数字を含めないよう注意してください。
(1) wget http://sourceforge.net/projects/webiopi/files/WebIOPi-0.7.1.tar.gz
(2) tar zxf WebIOPi-0.7.1.tar.gz
(3) cd WebIOPi-0.7.1/
(4) wget https://raw.githubusercontent.com/neuralassembly/raspi2/master/webiopi-pi2bplus.patch
(5) patch -p1 -i webiopi-pi2bplus.patch
(6) sudo ./setup.sh
なお、(6) のコマンド終了後に 「Do you want to access WebIOPi over Internet ? [y/n]」 という質問が出た場合は、キーボードの n をタイプして Enter するのでした(現在はこの質問は出ません)。

WebIOPi のインストールが終了したらそのまま以下の 2 つのコマンドも一つずつコピー&貼り付けにより確実に実行しましょう。
(1) wget https://raw.githubusercontent.com/neuralassembly/raspi2/master/webiopi.service
(2) sudo mv webiopi.service /etc/systemd/system/
なお、一度 WebIOPi のインストールに失敗するなどして、もう一度やり直したい場合、以下の 2 つのコマンドをコピー&貼り付けにより順番に実行し、WebIOPi のファイルを一度削除してからインストールコマンドを再実行しましょう。
(1) sudo rm -f WebIOPi-0.7.1.tar.gz
(2) sudo rm -rf WebIOPi-0.7.1

p.230, WebIOPi の起動コマンド

sudo service webiopi start

p.230, WebIOPi が起動されているか確認するためのコマンド

ps ax |grep webiopi 

p.231, WebIOPi の停止コマンド

sudo service webiopi stop

p.232, WebIOPi を自動起動するためのコマンドとその解除コマンド

sudo systemctl enable webiopi
sudo systemctl disable webiopi

p.234, IP アドレスを調べるためのコマンド

ifconfig

p.234:IPアドレスでのURLの指定について

本書では、ブラウザからRaspberry Piにアクセスする際に、ルーターなどからRaspberry Piに割り振られたIPアドレスを用いました。すなわち、IPアドレスが192.168.1.3の場合にブラウザから例えば下記のようにアクセスしました。
  • http://192.168.1.3:8000/bb/01/
しかし、この方法はifconfigコマンドなどで事前にIPアドレスを調べておく必要があり、やや面倒でした。
このIPアドレスの指定を簡単化する方法をPDFの付録Eに記しましたが、ここにも記しておきます。

以下の環境では、IPアドレスを用いずに「raspberrypi.local」のような名前でRaspberry Piにアクセスできます。
  • iTunesをインストールしたWindows(iTunesに含まれるBonjourというアプリケーションが必要なためです)
  • macOS
  • iPhoneやiPad
上記の環境では、ブラウザのアドレス欄に例えば下記のように入力すればIPアドレスを用いずにアクセスできます。
  • http://raspberrypi.local:8000/bb/01/
これは、Raspberry Pi OS上で動作しているavahiというソフトウェアの働きによります。

p.241, サンプルファイルを WebIOPi のフォルダにコピーするコマンド

このコマンドを実行し忘れると、9章、10章の演習を実行できませんので確実に実行しましょう。 2つのコマンドがありますので、 1つずつ順番に実行してください。サンプルファイルが存在するフォルダで実行する必要がありますので、サンプルファイルを bluebacks フォルダに保存した方は、事前に「cd bluebacks」コマンドを実行しておく必要があります。
sudo chown -R $USER /usr/share/webiopi/htdocs
cp -r 09-samples/bb /usr/share/webiopi/htdocs

p.242, WebIOPi の設定ファイルを管理者権限のテキストエディタで開くコマンド

sudo mousepad /etc/webiopi/config
なお、NOOBS 3.2.1 (Raspbian 2019-09-26) より前の OS ではテキストエディタとしてleafpadではなくmousepadを用います。
sudo leafpad /etc/webiopi/config

p.243, WebIOPi のパスワードを変更するためのコマンド

sudo webiopi-passwd

p.245, WebIOPi の設定ファイルに追加する内容

こちらの記述も、間違えると9章、10章の演習を実行できません。具体的には、記述間違いにより WebIOPi が起動しなくなります。そのため、コピー&貼り付けにより確実に記述するのが良いでしょう。
myscript = /usr/share/webiopi/htdocs/bb/01/script.py
なお、この行を記述した後に WebIOPi が起動しなくなった場合、行の記述し間違い以外には、
  • サンプルファイルを /usr/share/webiopi/htdocs ディレクトリにコピーしていない
という原因もありえます。その場合、本ページ上部の「p.241, サンプルファイルを WebIOPi のフォルダにコピーするコマンド」を参照してください。

10章

p.287, /etc/rc.local を管理者権限のテキストエディタで開くコマンド

sudo mousepad /etc/rc.local
なお、NOOBS 3.2.1 (Raspbian 2019-09-26) より前の OS ではテキストエディタとしてleafpadではなくmousepadを用います。
sudo leafpad /etc/rc.local

p.287, IPアドレスをLCDに表示するために /etc/rc.local に記入するコマンド

python3 /home/pi/07-02-LCD.py $_IP
なお、2022年4月にリリースされた OS よりデフォルトユーザー pi は廃止されています。そのため、上のコマンドの pi の部分は、皆さんが作成したユーザー名で置き換変える必要があります。すなわち、「kanamaru」というユーザーを作成したのなら、「python3 /home/kanamaru/07-02-LCD.py $_IP」となる、ということです。

また、サンプルファイルを bluebacks ディレクトリに展開した方の場合、記入する行は以下です。
python3 /home/pi/bluebacks/07-02-LCD.py $_IP
上と同様に、「pi」は自分のユーザー名に置き換えてください。

p.291, シャットダウンプログラムの自動実行のために /etc/rc.local に記入するコマンド

末尾の「&」も忘れずにコピーしましょう。
python3 /home/pi/10-01-sw-poweroff.py &
上と同様に、「pi」は自分のユーザー名に置き換えてください。

なお、サンプルファイルを bluebacks ディレクトリに展開した方の場合、記入する行は以下です。
python3 /home/pi/bluebacks/10-01-sw-poweroff.py &
上と同様に、「pi」は自分のユーザー名に置き換えてください。

p.292:キャタピラ式模型のメンテナンス

キャタピラ式模型を長く使っていると、だんだん動作が安定しなくなってくることがあります。例えば「右のキャタピラは正常動作するが、左はなかなか動かない」などです。

もちろん、「ジャンパーワイヤーが抜けていないか」など、回路の配線をまずはチェックすべきです。 しかし回路の配線に問題がなかった場合、どこに注意してメンテナンスすべきかを以下に記します。上から順にチェックしていきましょう。
  1. 電池の残量不足ではないかチェック:基本ではありますが、まずはここからチェックしましょう。
  2. ジャンパワイヤのチェック:ジャンパワイヤの抜き差しをする際、持ち手ではなくケーブルを引っ張ることを繰り返すと中で断線することがあります。断線したかどうかは外から見ただけではわかりません。断線が疑われるジャンパワイヤは捨ててしまった方がよいでしょう。
  3. モーターの軸が空回りしていないかチェック:モーターの軸にはピニオンギアが取り付けられています。ピニオンギアが劣化してゆるくなり、モーターの軸が空回りすることがしばしば起こります。これはモーターをギアボックスから取り外してみないとわかりません。ピニオンギアがゆるくなっていた場合、「AO-7005 8Tピニオンセット白(10個)」などを購入して交換しましょう。
  4. ギアボックスで空回りしているシャフトがないかチェック:ギアボックスの作成の際、イモネジを六角レンチで締めてシャフトを固定している部分があります。そこが緩んで空回りしていないかチェックしましょう。
  5. ブレッドボードのチェック:ブレッドボードを長くつかっていると、なんらかの理由でブレッドボード内部の抵抗が大きくなりモーターがあまり回転しなくなることがあります。筆者は複数回そのような経験をしています。安価なもので良いのでテスターを購入すると内部の抵抗の大きさをチェックできます。内部の抵抗が大きく計測されたブレッドボードは消耗品と考えて捨ててしまった方がよいでしょう。
  6. モーターのチェック:上のどの問題にも該当しなかった場合、モーターに問題がある可能性があります。新品の「AO-1001 FA-130タイプノーマルモーター」を購入して交換してみるのも手です。
  7. モータードライバーのチェック:上のどの問題にも該当しなかった場合、モータードライバに問題がある可能性があります。新品のモータードライバーを購入して交換してみるのも手です。

p.293, mjpg-streamer のインストールコマンド

mjpg-streamer のインストールコマンドは下記の通りです。全てのコマンドが長いので、一つずつ順番にコピーして確実に実行していきましょう。

ただし、Bookworm や Bullseye を用いていて、libcamera 対応の mjpg-streamer をインストールする場合は、ここで紹介するコマンドではなく、「本書の演習を picamera2 (libcamera) で実行する方法」で紹介するコマンドを実行する必要がありますのでページを移動してください。

さて、ここで紹介するコマンドは、libcamera を用いない場合すなわち、Bullseye の Legacy Camera モードでカメラを利用する場合です。 コピーの際、先頭の「(1)」、「(2)」などの数字を含めないよう注意してください。
(1) sudo apt update
(2) sudo apt install libjpeg-dev cmake
(3) git clone https://github.com/neuralassembly/mjpg-streamer.git
(4) cd mjpg-streamer/mjpg-streamer-experimental
(5) make
(6) cd
(7) sudo mv mjpg-streamer/mjpg-streamer-experimental /opt/mjpg-streamer
なお、古い OS を用いている方は、(2) のコマンドを
(2) sudo apt install libjpeg8-dev cmake
に変更しなければならいない場合があります。

また、 Bullseye を用いている方は、本ページの 「p.128, Bookworm (Raspberry Pi OS 2023-10-10 以降) および Bullseye (Raspberry Pi OS 2021-10-30 以降) でのカメラの利用について」の項目を参考に、Legacy Camera モードを有効にする必要がありますのでその点もご注意ください。 さらに、64-bit版 Bullseye を用いている方は、実行時に「本書の演習をウェブカメラで実行する方法」で配布しているサンプルファイル内の 10-02-stream-webcam.sh を用いる必要があります。

また、一度 mjpg-streamer のインストールに失敗するなどして、もう一度やり直したい場合、以下の 2 つのコマンドをコピー&貼り付けにより順番に実行し、mjpg-streamer のファイルを一度削除してからインストールコマンドを再実行しましょう。
(1) sudo rm -rf /opt/mjpg-streamer
(2) rm -rf mjpg-streamer

p.294, mjpg-streamer の自動実行のために /etc/rc.local に記入するコマンド

sh /home/pi/10-02-stream.sh
なお、2022年4月にリリースされた OS よりデフォルトユーザー pi は廃止されています。そのため、上のコマンドの pi の部分は、皆さんが作成したユーザー名で置き換変える必要があります。すなわち、「kanamaru」というユーザーを作成したのなら、「sh /home/kanamaru/10-02-stream.sh」となる、ということです。

また、サンプルファイルを bluebacks ディレクトリに展開した方の場合、記入する行は以下です。
sh /home/pi/bluebacks/10-02-stream.sh
上と同様に、「pi」は自分のユーザー名に置き換えてください。

付録

p.308, サンプルファイルの展開先(図B-4)

/home/pi/bluebacks
なお、2022年4月にリリースされた OS よりデフォルトユーザー pi は廃止されています。そのため、上の記述の pi の部分は、皆さんが作成したユーザー名で置き換変える必要があります。すなわち、「kanamaru」というユーザーを作成したのなら、「/home/kanamaru/bluebacks」となる、ということです。

p.310, nano の設定ファイル .nanorc に記す内容

set tabsize "4"
set tabstospaces

p.310, vi の設定ファイル .vimrc に記す内容

set expandtab
set tabstop=4
set softtabstop=4
set shiftwidth=4

p.312, 日本語入力ソフトウェアのインストールコマンド

日本語入力ソフトウェアのインストールについて、これまで本書では ibus-mozc のインストールをお勧めしてきましたが、OS の更新に伴い メニューバー上での挙動がおかしい、などの不具合が増えてきましたので、 別のソフトウェアである fcitx-mozc に切り替えた方が良いかもしれません。

まず、インストール済の ibus-mozc を削除するには以下のコマンドを実行します。
sudo apt remove ibus-mozc ibus
その後 Raspberry Pi を再起動することで ibus-mozc の削除が完了します。

そして、fcitx-mozc のインストールは下記のコマンドで行います。
sudo apt update

sudo apt install fcitx-mozc
再起動後、半角/全角キーや Ctrl+スペースキーにより、日本語入力をオンオフできます。

p.313, 日本語フォントのインストールコマンド

sudo apt install fonts-vlgothic

補足PDF

PDF9, 06-03-volume.py で音声のボリュームを変更する

プログラム 06-03-volume.py で音声のボリュームを変更する場合、音声出力先や、用いているOSのバージョンにより方法が異なりますので以下の指示に従ってください。

<2022年 9 月以降の Raspberry Pi OS をご利用の場合>

2022年 9 月以降の Raspberry Pi OS をご利用の場合、音声出力先がHDMI、イヤフォンジャックのどちらの場合も、ファイル 06-03-volume.py の変更が必要となります。

まず、イヤフォンジャックから音声を出力したい場合、本ページ「p.134, 音声の出力先を変更する方法」の方法に基づき、音声の出力先をイヤフォンジャックに変更して下さい。HDMI経由で音声出力したい場合はデフォルトのままで構いません。
すると、音声の出力先がどちらであっても、以下のデフォルトのコマンドで音声が鳴ります。
mplayer test.mp3
また、音声の出力先がどちらであっても、06-03-volume.py に対して変更が必要になります。Thonny でプログラムを開くと 57、58行目に以下の行が見つかります。
            args = ['amixer','-q','cset','numid=1',vol]
            #args = ['amixer','-q','-c1','cset','numid=1',vol]
これを以下のように変更して保存して下さい。「numid=1」の部分を「numid=3」に変更しています。
            args = ['amixer','-q','cset','numid=3',vol]
            #args = ['amixer','-q','-c1','cset','numid=1',vol]
以上で、06-03-volume.py によりボリュームを変更可能になります。

<2020-12-02版から 2022年春までの Raspberry Pi OS をご利用の場合>

2020-12-02版から 2022年春までの Raspberry Pi OS をご利用の場合、音声出力先がHDMI、イヤフォンジャックのどちらの場合も、ファイル 06-03-volume.py の変更が必要となります。

まず、イヤフォンジャックから音声を出力したい場合、本ページ「p.134, 音声の出力先を変更する方法」の方法に基づき、音声の出力先をイヤフォンジャックに変更して下さい。HDMI経由で音声出力したい場合はデフォルトのままで構いません。
すると、音声の出力先がどちらであっても、以下のデフォルトのコマンドで音声が鳴ります。
mplayer test.mp3
また、音声の出力先がどちらであっても、06-03-volume.py に対して変更が必要になります。Thonny でプログラムを開くと 57、58行目に以下の行が見つかります。
            args = ['amixer','-q','cset','numid=1',vol]
            #args = ['amixer','-q','-c1','cset','numid=1',vol]
これを以下のように変更して保存して下さい。元からあった行に「#」をつけて無効化し、その上に一行追加しています。
            args = ['amixer','-q','-D','pulse','set','Master',vol]
            #args = ['amixer','-q','cset','numid=1',vol]
            #args = ['amixer','-q','-c1','cset','numid=1',vol]
以上で、06-03-volume.py によりボリュームを変更可能になります。

<2020-5-27版および2020-8-20版の Raspberry Pi OS をご利用の場合>

2020-5-27版および2020-8-20版の Raspberry Pi OS で音声出力先をイヤフォンジャックにしたい場合、 書籍に記述してある通りに実行します。すなわち、音声再生コマンドを以下のコマンドに置き換えるのでした。
mplayer -ao alsa:device=hw=1,0 test.mp3
そして、06-03-volume.py に対しては、変更が必要となるのでした。Thonny でプログラムを開くと、57、58行目に以下の行が見つかります。
            args = ['amixer','-q','cset','numid=1',vol]
            #args = ['amixer','-q','-c1','cset','numid=1',vol]
一つ目の行が実際に実行される行であり、二つ目の行は「#」があるので無効な行です。これを、以下のように「#」の有無が逆になるよう編集してファイルを保存してください。
            #args = ['amixer','-q','cset','numid=1',vol]
            args = ['amixer','-q','-c1','cset','numid=1',vol]
以上で、06-03-volume.py によりボリュームを変更可能になります。

PDF 11, デジタル温度計の自動実行のために /etc/rc.local に記すコマンド

末尾の「&」も忘れずにコピーしましょう。
python3 /home/pi/07-03-LCD-temp.py &
なお、2022年4月にリリースされた OS よりデフォルトユーザー pi は廃止されています。そのため、上のコマンドの pi の部分は、皆さんが作成したユーザー名で置き換変える必要があります。すなわち、「kanamaru」というユーザーを作成したのなら、「python3 /home/kanamaru/07-03-LCD-temp.py &」となる、ということです。

また、サンプルファイルを bluebacks ディレクトリに展開した方の場合、記入する行は以下です。
python3 /home/pi/bluebacks/07-03-LCD-temp.py &
上と同様に、「pi」は自分のユーザー名に置き換えてください。

PDF 29, DCモーターとサーボモーターを同時に使うとサーボモーターが不安定になる場合(図10-13)

図10-13のようにDCモーターとサーボモーターを同時に使うとサーボモーターが不安定になる場合があります。

これは、DCモーターの回転により発生するノイズがサーボモーターに影響を与えているためです。この問題に対する簡単な対処法は、下図のように サーボモーターの電源用配線を、Raspberry Pi の 5V ピンに接続することです。
このような接続をすると、今度はサーボモーターの負荷により Raspberry Pi が不安定になる可能性がありますが、以下の条件が満たされれば Raspberry Pi は安定して動作します。
  • サーボモーターにあまり大きな負荷をかけない。この演習の例では、Raspberry Pi のカメラモジュールという軽いものを動かすだけなので問題にはなりにくいでしょう
  • Raspberry Piを動作させるためのモバイルバッテリーの流せる電流量(「A」や「mA」で表示されているもの)が大きい。筆者は Raspberry Pi 4 に対して 3.0 A のバッテリーを用意しました
なお、この DC モーターによるノイズの問題に関しては、本書p.213の図8-12のように、DCモーターに3つのコンデンサを取り付けることでも改善が期待できます。

本書の演習を picamera2 (libcamera) で実行する方法

1. はじめに

Raspberry Pi OS 2023-10-10 からはじまったバージョン Bookworm および
Raspberry Pi OS 2021-10-30 からはじまったバージョン Bullseye では
カメラモジュールの利用方法に大きな変更が加えられ、libcamera というライブラリを用いるようになりました。 これは、本書でカメラを用いるプログラムがそのままでは動かなくなる、ということを意味します。

ただし、Bullseye では「Legacy Camera」という、これまでと互換性のあるカメラの利用方法が可能になっています。この Lecgacy Camera を用いれば本書でカメラを用いるプログラムはそのまま動作しますが、 残念ながら Bookworm では Legacy Camera モードは削除されてしまいました。

そこで、本ページでは、Legacy Camera に頼らずに最新の OS で本書の演習を実行する方法を解説します。具体的には、libcamera ライブラリを利用する Python モジュールである picamera2 を使ってプログラムを実行します。

本書のサンプルプログラムは picamera の旧バージョンを使って書かれていました。しかし、picamera2 は picamera とは互換性がありませんので、picamera2 用に書き換えられたプログラムをダウンロードして利用することになります。

2. 準備

サポート環境は Bookworm および Bullseye 以の 32-bit または 64-bit 版の Raspberry Pi OS です。Bullseye の場合は、なるべく新しいバージョンを用いましょう。そうしないと、本ページで利用する picamera2 (python3-picamera2) がインストールされていないことがあるからです。

また、Bullseye の場合、Legacy Camera モードは無効にしておく必要があります。OS インストール直後の状態ではあらかじめ無効になっています。一度有効にしてしまった方は、 ターミナルを開いて以下の手順に従うことで、raspi-config により Legacy Camera モードを無効にしましょう。
  1. 「 sudo raspi-config 」を実行することで raspi-config を起動
  2. キーボードの「↓」キーを2回押し、「Interface Options」を選択してから「Enter」キーを押す
  3. 「I1 Legacy Camera Enable/Disable」があらかじめ選択されているので、「Enter」キーを押す
  4. 「Would you like to enable legacy camera support?」と聞かれるので「→」キーで「いいえ」が選択された状態にし、「Enter」キーを押す
  5. 「Legacy camera support is disabled」と表示されるので、「了解」が選択された状態で「Enter」キーを押す
  6. raspi-configを起動した状態の画面に戻るので、「TAB」キーを二回クリックし、「Finish」を選択した状態で「Enter」キーを押す
  7. 再起動を促されるので、「はい」が選択された状態で「Enter」キーを押す
以上で、Legacy Camera モードが無効になります。

次に、お使いのカメラモジュールのバージョンを把握してください。2023年3月現在、Raspberry Pi のカメラモジュールにはバージョン1から3があり、下図のような外観をしています。
左から、バージョン1(基板が四角)、バージョン2(基板の角が丸く、レンズ周辺が黒)、バージョン3(基板の角が丸く、レンズ周辺が銀)です。基板上にもバージョンの記載がありますね。

後でダウンロードして頂くプログラムでは、このカメラモジュールのバージョンにより設定が変わる箇所があります。

3. ダウンロードと準備

それでは、picamera2 を利用するプログラムをダウンロードしましょう。

ターミナルを開き、本書のサンプルファイル(数字で始まるファイル)が存在するディレクトリに移動してください。 サンプルファイルをホームディレクトリに展開した方は移動の必要はありません。bluebacksディレクトリに展開した方は下記のコマンドを実行するのでした。
cd bluebacks
次に、下記の3つのコマンドを順に実行して、必要なファイルのダウンロードと展開を行ってください。コマンドはコピー&貼り付けで実行することを推奨します。展開後はダウンロードした圧縮ファイルは不要となるので削除しています。
wget https://github.com/neuralassembly/raspi/raw/master/raspi1a-picamera2.zip
unzip raspi1a-picamera2.zip
rm raspi1a-picamera2.zip
なお、展開されるファイルの名前は既存のサンプルファイルとは異なりますので、サンプルファイルが上書きされることはありません。展開により現れるファイルは下記の通りです。Pythonpプログラムにはファイル名末尾に「-picamera2.py」がついていることに注意してください。
05-04-sw-camera-picamera2.py
10-02-stream-libcamera.sh
以下では、これらのファイルの使い方を解説していきます。

4. 実行方法

さて、ダウンロードして展開したファイルの使い方を解説していきます。

p.128 タクトスイッチをカメラのシャッターに (5.6)

5.6章ではタクトスイッチをカメラのシャッターにするため、プログラムでカメラの映像を表示する必要がありました。

そのプログラムの picamera2 版が 05-04-sw-camera-picamera2.py です。このプログラムを実行するには、画像処理用のライブラリである OpenCV をインストールする必要があります。 ターミナルを起動して下記のコマンドを実行すると、OpenCV をインストールできます。
sudo apt update
sudo apt install python3-opencv
プログラムは、下記のコマンドで(または Thonny で)実行します。
python3 05-04-sw-camera-picamera2.py
なお、libcamera.so.0.X.X のバージョンに関するエラーが出た場合、下記のコマンドで libcamera-dev と python3-picamera2 を更新すると良いかもしれません。
sudo apt update
sudo apt install libcamera-dev python3-picamera2


p.292 mjpg-streamer の代替

映像配信用に、mjpg-streamer をインストールして用いますが、これも libcamera に対応したバージョンをインストールする必要があります。

libcamera 対応の mjpg-streamer は ArduCAM さんが公開していますが、これをこのまま用いると、 本書のように低解像度 (640x480) の映像を指定したときに以下の問題が現れます。
  • 映像の画質が非常に悪い (カメラモジュール v.1)
  • 映像の範囲が非常に狭い (カメラモジュール v.2 と 3)
この問題を筆者が独自に修整した版をここでは利用することにします。

まず、これまでダウンロードまたはインストールした mjpg-streamer を削除するため、ターミナルを起動して以下のコマンドを実行しましょう。
sudo rm -rf mjpg-streamer /opt/mjpg-streamer
そして、以下の手順で修整済の libcamera 対応 mjpg-streamer をインストールしましょう。
(1) sudo apt update
(2) sudo apt install libjpeg-dev cmake libcamera-dev
(3) git clone https://github.com/neuralpi/mjpg-streamer.git
(4) cd mjpg-streamer/mjpg-streamer-experimental
(5) make
(6) cd
(7) sudo mv mjpg-streamer/mjpg-streamer-experimental /opt/mjpg-streamer
なお、OS として Bullseye やリリース直後の Bookworm をお使いの方は、上記の (5) で LibCamera.cpp のビルド中にエラーが起こると思います。その場合、エラーが出た状態から以下の 8 コマンドを一つずつ順に実行してください。この 8 コマンドが上記 (5)~(7) の代替、というイメージです。
rm -rf _build
mkdir _build
cd _build
cmake -DLIBCAMERA_USES_TRANSFORM=ON ..
cd ..
make
cd
sudo mv mjpg-streamer/mjpg-streamer-experimental /opt/mjpg-streamer
インストール後は、上で展開して得られたシェルスクリプト 10-02-stream-libcamera.sh を実行することで mjpg-streamer を起動しますが、 実行前に一つ注意があります。 10-02-stream-libcamera.sh をテキストディタで開くと以下のような箇所があります。
  LD_LIBRARY_PATH=/opt/mjpg-streamer/ /opt/mjpg-streamer/mjpg_streamer -i "input_libcamera.so -camver 1 -fps 15 -r 640x480 -s 640x480" -o "output_http.so -p 9000 -w /opt/mjpg-streamer/www" > /dev/null 2>&1&
このうち
-camver 1
の部分が、筆者が修整した部分に関連しており、カメラモジュールのバージョンの数字を指定しています。お使いのカメラモジュールがバージョン 2 か 3 なら、この数字を 2 または 3 に変更し、それからファイルを保存してください。 なお、この数値を 0 にするかあるいは -camver の設定自体を削除すると ArduCAM さんの mjpg-streamer と同じ挙動になります。

保存が済んだら、コマンド
sh 10-02-stream-libcamera.sh
を実行することで mjpg-streamer が起動されます。

以上、お疲れさまでした。

2023年3月28日火曜日

Raspberry Pi Pico W を使って Wifi 経由で回路を制御してみよう

0. はじめに

本書の電子工作パーツをRaspberry Pi Picoで利用する」にて、本書の公式パーツセットを Raspberry Pi Pico (以下 Pico)で利用する方法を紹介しました。

Wifi 機能のない Raspberry Pi Pico で実行できる演習は、 本書で Wifi 接続を用いない 1 章から 8 章の内容でした。

一方、2023年3月、Wifi 機能付きの Raspberry Pi Pico W (以下 Pico W)が日本で販売開始されました。Pico W を用いると、 Wifi 接続を用いる 9 章および 10 章の演習をある程度再現できるようになります。 具体的には、本ページでは下記の内容を取り扱います。
  • 9章の内容: PC やスマートフォンのブラウザで、Pico W に接続された LED / I2C温度センサ / RGB フルカラー LED / DCモータ / サーボモータ を制御
  • 10章の内容: PCやスマートフォンのブラウザで、Pico W を搭載したキャタピラ式模型を制御
10章のキャタピラ式模型を Pico W で実現した様子を示したのが下図です。もともと Raspberry Pi が配置されていたスペースが空いていますが、これは 回路と Pico W を全てブレッドボード上に配置できるからです。回路動作用のバッテリーが、大電流を流せないものでも良い、というメリットもあります。
そして、このキャタピラ式模型の動作の様子が下記の YouTube 動画です。



なお、Wifi 経由での Pico W の制御には欠点もあり、回路への制御信号の送信が、ブラウザのページ再読み込みのタイミングでしか行えないため、ブラウザに対して行ったアクションへのレスポンスが悪い、という問題があります。

さて、本ページでは以上の内容の実現方法を解説していきます。

その内容に進む前に、「本書の電子工作パーツをRaspberry Pi Picoで利用する」を参考に、 下記の内容を進めておいてください。Pico W でも、Pico 用の演習をすべて実行することができます。Pico W を購入できるサイトもリンク先で紹介されています。
  • Pico W への MicroPython 環境のインストール (Pico と Pico W では UF2 ファイルが異なるので注意)
  • Windows などへの、開発環境 Thonny のインストール
  • Pico W で Python プログラムを実行する方法の習得

1. ブラウザのボタンによるLEDの点灯

まずは、PCやスマートフォンのブラウザから、Pico W に接続された LED のオン / オフを切り替える演習を行ってみましょう。9.3 章の内容です。必要な回路は下図の通りです。
見てわかる通り、LED以外に I2C 接続の小型 LCD も接続されています。これは、Pico W に割り当てられた IP アドレスやエラーメッセージを表示するためのものです。この LCD を接続しなくても演習は実行可能ですが、その場合、Pico W からのメッセージを Windows などの PC 上で見るしかなくなります。LCD を接続すること、すなわち Windows などの PC なしでも Pico W を動作させられるようにすることを強く推奨します。

さて、この回路に対して実行するプログラムは以下の通りです。なお、本ページで紹介するプログラムはすべてこちらのページの解説を参考に作成しました。
import time
import sys
import network
import socket
from time import sleep
from machine import Pin, I2C

led = Pin(16, Pin.OUT)
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
i2c.scan()

ssid = 'YOUR_WIFI_SSID'
password = 'YOUR_WIFI_PASSWORD'

##### for LCD
def setup_st7032():
    c_lower = (contrast & 0xf)
    c_upper = (contrast & 0x30)>>4
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x39, 0x14, 0x70|c_lower, 0x54|c_upper, 0x6c]))
    sleep(0.2)
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x0d, 0x01]))
    sleep(0.001)

def clear():
    global position
    global line
    position = 0
    line = 0
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x01]))
    sleep(0.001)

def newline():
    global position
    global line
    if line == display_lines-1:
        clear()
    else:
        line += 1
        position = chars_per_line*line
        i2c.writeto_mem(address_st7032, register_setting, bytes([0xc0]))
        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()
    i2c.writeto_mem(address_st7032, register_display, bytes([byte_data]))
    position += 1

def check_writable(c):
    if c >= 0x06 and c <= 0xff :
        return c
    else:
        return 0x20 # 空白文字

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()
##### end of LCD

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

html = """<!DOCTYPE html><html>
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>1 - LEDの点灯制御</title>
<style>button {
    width: auto;
    height: auto;
    background: %s;
    font-weight: normal;
    font-size: 14pt;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 14px;
    color: #ffffff;
    padding: 10px 20px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: 1px solid #003366;
    -moz-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    -webkit-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    text-shadow:
        0px -1px 0px rgba(000,000,000,0.7),
        0px 1px 0px rgba(255,255,255,0.3);
}
</style></head>
<p>下のボタンを押すごとに、LEDの点灯状態が変化します。</p>
<div align="center">
<form><button name="led" value="toggle" type="submit">LED</button></form>
</div>
</body></html>
"""

# Wait for connect or fail
max_wait = 20
while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    max_wait -= 1
    print('waiting for connection...')
    write_string('waiting for conn') #showing on LCD
    time.sleep(1)

# Handle connection error
if wlan.status() != 3:
    write_string('conn. failed') # showing on LCD
    raise RuntimeError('network connection failed')
else:
    print('Connected')
    status = wlan.ifconfig()
    print( 'ip = ' + status[0] )
    write_string(status[0]) #showing on LCD

# Open socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(addr)
except OSError as e:
    print(e)
    s.close()
    s = None
    write_string('socket error') #showing on LCD
    sys.exit()

s.listen(1)
print('listening on', addr)

ledState = 0
buttonColor = '#003366'

# Listen for connections, serve client
while True:
    try:
        cl, addr = s.accept()
        #print('client connected from', addr)
        request = cl.recv(1024)
        #print("request:")
        #print(request)
        request = str(request)
        led_toggle = request.find('led=toggle')

        #print( 'led_toggle = ' + str(led_toggle))

        if led_toggle == 8:
            #print("led toggle")
            ledState = 1 - ledState
            led.value(ledState)
            if ledState == 0:
                buttonColor = '#003366'
            else:
                buttonColor = '#26a1ff'

        response = html % buttonColor
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        print('connection closed')

    except KeyboardInterrupt as e:
        break

s.close()
s = None
このプログラムを実行するための注意をいくつか記します。本ページのプログラムにはすべて下記のように、用いている Wifi の SSID (アクセスポイント名) とそのパスワードを記す項目があります。 ここを皆さんの環境における SSID とパスワードに変更しないと本ページのプログラムは全て動作しませんのでご注意ください。なお、2.4GHzの周波数帯の Wifi アクセスポイントにしか接続できませんのでご注意ください。
ssid = 'YOUR_WIFI_SSID'
password = 'YOUR_WIFI_PASSWORD'
また、本ページの回路を LCD なしで実行する場合、プログラム中の下記の部分を消し、
##### for LCD
(省略)
##### End of LCD
下記の内容で差し替える必要があります。この変更も、本ページのすべてのプログラムで共通です。
def write_string(s):
    pass
さらに、プログラムの書き換え時は、Pico W 上で動作しているプログラムを Thonny の「STOP (停止)」ボタンから一旦停止する必要があることもご注意ください。

記述できたら Pico W でプログラムを実行してみましょう。LCD 上に(または Thonny のコンソール上に)Pico W に割り当てられた IP アドレス(例えば、 192.168.1.40 のようなもの)が表示されれば、実行が成功した可能性が高いでしょう。下図のような状態です。
そうしたら、Pico W と同じネットワーク内に存在する PC やスマートフォンのブラウザでアドレス(例えば 192.168.1.40 の場合、 http://192.168.1.40/ )にアクセスしてみましょう。下図のようなページが現れるはずです。
ブラウザ上のボタンを押すことで Pico W に接続された LED の点灯/消灯が切り替わります。LED 点灯時の画面の様子は下記の通りで、ボタンの色が変わります。これは、本書の Raspberry Pi 版のプログラムと同じ挙動です。ただし、上で既に述べたように、回路のアクションはブラウザ上のページの再読み込みのタイミングでしか起こりませんので、反応が遅いという問題がありますのでご了承ください。これは、本ページの以下のプログラム全てに当てはまります。
なお、ここでの解説がよくわからないという場合、本書 9 章の記述を読み直してみることを推奨します。本ページは、本書読者の方を対象としているため、本書と重複する内容の記述は最低限にとどめています。

2. ブラウザへの温度センサの値の表示

PCやスマートフォンのブラウザに、Pico W に接続された I2C 温度センサの値を表示する演習を行ってみましょう。9.4 章の内容です。必要な回路は下図の通りです。
そして、この回路上で実行すべきプログラムは下記の通りです。
import time
import sys
import network
import socket
from time import sleep
from machine import Pin, I2C

ssid = 'YOUR_WIFI_SSID'
password = 'YOUR_WIFI_PASSWORD'

i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
i2c.scan()

##### for temperature sensor
def read_adt7410():
    word_data = int.from_bytes(i2c.readfrom_mem(address_adt7410, register_adt7410, 4), 'little')
    data = (word_data & 0xff00)>>8 | (word_data & 0xff)<<8
    data = data>>3 # 13ビットデータ
    if data & 0x1000 == 0:  # 温度が正または0の場合
        temperature = data*0.0625
    else: # 温度が負の場合、 絶対値を取ってからマイナスをかける
        temperature = ( (~data&0x1fff) + 1)*-0.0625
    return temperature

address_adt7410 = 0x48
register_adt7410 = 0x00
##### end of temperature sensor

##### for LCD
def setup_st7032():
    c_lower = (contrast & 0xf)
    c_upper = (contrast & 0x30)>>4
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x39, 0x14, 0x70|c_lower, 0x54|c_upper, 0x6c]))
    sleep(0.2)
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x0d, 0x01]))
    sleep(0.001)

def clear():
    global position
    global line
    position = 0
    line = 0
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x01]))
    sleep(0.001)

def newline():
    global position
    global line
    if line == display_lines-1:
        clear()
    else:
        line += 1
        position = chars_per_line*line
        i2c.writeto_mem(address_st7032, register_setting, bytes([0xc0]))
        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()
    i2c.writeto_mem(address_st7032, register_display, bytes([byte_data]))
    position += 1

def check_writable(c):
    if c >= 0x06 and c <= 0xff :
        return c
    else:
        return 0x20 # 空白文字

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()
##### end of LCD

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

# refresh per 5 sec.
html = """<!DOCTYPE html><html>
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta http-equiv="refresh" content="5">
<title>2 - I2C温度センサ(ADT7410)による温度の取得</title>
</head>
<div align="center">
<p>温度: %s</p>
</div>
</body></html>
"""

# Wait for connect or fail
max_wait = 20
while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    max_wait -= 1
    print('waiting for connection...')
    write_string('waiting for conn') #showing on LCD
    time.sleep(1)

# Handle connection error
if wlan.status() != 3:
    write_string('conn. failed') # showing on LCD
    raise RuntimeError('network connection failed')
else:
    print('Connected')
    status = wlan.ifconfig()
    print( 'ip = ' + status[0] )
    write_string(status[0]) #showing on LCD

# Open socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(addr)
except OSError as e:
    print(e)
    s.close()
    s = None
    write_string('socket error') #showing on LCD
    sys.exit()

s.listen(1)
print('listening on', addr)

# Listen for connections, serve client
while True:
    try:
        cl, addr = s.accept()
        #print('client connected from', addr)
        request = cl.recv(1024)
        #print("request:")
        #print(request)
        request = str(request)
        inputValue = read_adt7410()
        inputStr = '{:.1f}'.format(inputValue)

        response = html % inputStr
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        print('connection closed')

    except KeyboardInterrupt as e:
        break

s.close()
s = None
なお、「1. ブラウザのボタンによるLEDの点灯」で述べたように、YOUR_WIFI_SSID と YOUR_WIFI_PASSWORD を自分の環境のものに変更することと、 LCD なしでプログラムを実行したい場合は「# for LCD ~ # End of LCD」の部分の差し替えが必要ですのでご注意ください。

さらに、プログラムの書き換え時は、Pico W 上で動作しているプログラムを Thonny の「STOP (停止)」ボタンから一旦停止する必要があることもご注意ください。

以上のプログラムを実行し、ブラウザでアクセスすると下記のようなページが開きます。
5秒おきにページが再読み込みされ、センサの値がページ上で更新されるようになっています。 これは、プログラム中の下記の部分で設定されています。あまり頻繁に通信が行われるのも良くないですので、この部分を小さな値に変更することはお勧めしません。
<meta http-equiv="refresh" content="5">


3. ブラウザのボタンによるRGBフルカラーLED の制御

PCやスマートフォンのブラウザで、Pico W に接続された RGBフルカラーLED の色を制御する演習を行ってみましょう。9.5 章の内容です。

なお、Rapsberry Pi を対象とした本書では、RGBフルカラーLEDの色の制御に、ブラウザのスライダを用いました。 しかし、Pico W ではスライダを用いるのは難しそうに思えましたので(専門的に言えば、JavaScript で取得したスライダの値を回路に反映する方法が思いつかない)、簡易的にボタンによる制御を行うことにしました。

必要な回路は下図の通りです。
そして、この回路上で実行すべきプログラムは下記の通りです。
import time
import sys
import network
import socket
from time import sleep
from machine import Pin, PWM, I2C

pwm1 = PWM(Pin(16))
pwm2 = PWM(Pin(17))
pwm3 = PWM(Pin(18))
pwm1.freq(100)
pwm2.freq(100)
pwm3.freq(100)
pwm1.duty_u16(0)
pwm2.duty_u16(0)
pwm3.duty_u16(0)
# アノードコモンの場合、下記の3行を有効に   
#pwm1.duty_u16(65535)
#pwm2.duty_u16(65535)
#pwm3.duty_u16(65535)

i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
i2c.scan()

ssid = 'YOUR_WIFI_SSID'
password = 'YOUR_WIFI_PASSWORD'

##### for LCD
def setup_st7032():
    c_lower = (contrast & 0xf)
    c_upper = (contrast & 0x30)>>4
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x39, 0x14, 0x70|c_lower, 0x54|c_upper, 0x6c]))
    sleep(0.2)
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x0d, 0x01]))
    sleep(0.001)

def clear():
    global position
    global line
    position = 0
    line = 0
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x01]))
    sleep(0.001)

def newline():
    global position
    global line
    if line == display_lines-1:
        clear()
    else:
        line += 1
        position = chars_per_line*line
        i2c.writeto_mem(address_st7032, register_setting, bytes([0xc0]))
        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()
    i2c.writeto_mem(address_st7032, register_display, bytes([byte_data]))
    position += 1

def check_writable(c):
    if c >= 0x06 and c <= 0xff :
        return c
    else:
        return 0x20 # 空白文字

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()
##### end of LCD

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

html = """<!DOCTYPE html><html>
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>3 - RGBフルカラーLEDの制御</title>
<style>button {
    width: auto;
    height: auto;
    background: #003366;
    font-weight: normal;
    font-size: 14pt;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 14px;
    color: #ffffff;
    padding: 10px 20px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: 1px solid #003366;
    -moz-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    -webkit-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    text-shadow:
        0px -1px 0px rgba(000,000,000,0.7),
        0px 1px 0px rgba(255,255,255,0.3);
}
</style></head>
<p>ボタンをクリックすることで、RGBフルカラーLEDの色が変わります。</p>
<p>どの行のボタンが赤、青、緑の何色に対応するかは用いるRGBフルカラーLEDの種類によって異なります。</p>
<div align="center">
<form>色 1 <button name="col1_0" value="on" type="submit" style="background: %s;"> 0 </button>
<button name="col1_1" value="on" type="submit" style="background: %s;"> 1 </button>
<button name="col1_2" value="on" type="submit" style="background: %s;"> 2 </button>
<button name="col1_3" value="on" type="submit" style="background: %s;"> 3 </button>
<button name="col1_4" value="on" type="submit" style="background: %s;"> 4 </button>
</form>
<br /><br />
<form>色 2 <button name="col2_0" value="on" type="submit" style="background: %s;"> 0 </button>
<button name="col2_1" value="on" type="submit" style="background: %s;"> 1 </button>
<button name="col2_2" value="on" type="submit" style="background: %s;"> 2 </button>
<button name="col2_3" value="on" type="submit" style="background: %s;"> 3 </button>
<button name="col2_4" value="on" type="submit" style="background: %s;"> 4 </button>
</form>
<br /><br />
<form>色 3 <button name="col3_0" value="on" type="submit" style="background: %s;"> 0 </button>
<button name="col3_1" value="on" type="submit" style="background: %s;"> 1 </button>
<button name="col3_2" value="on" type="submit" style="background: %s;"> 2 </button>
<button name="col3_3" value="on" type="submit" style="background: %s;"> 3 </button>
<button name="col3_4" value="on" type="submit" style="background: %s;"> 4 </button>
</form>
<br /><br />
</div>
</body></html>
"""

# Wait for connect or fail
max_wait = 20
while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    max_wait -= 1
    print('waiting for connection...')
    write_string('waiting for conn') #showing on LCD
    time.sleep(1)

# Handle connection error
if wlan.status() != 3:
    write_string('conn. failed') # showing on LCD
    raise RuntimeError('network connection failed')
else:
    print('Connected')
    status = wlan.ifconfig()
    print( 'ip = ' + status[0] )
    write_string(status[0]) #showing on LCD

# Open socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(addr)
except OSError as e:
    print(e)
    s.close()
    s = None
    write_string('socket error') #showing on LCD
    sys.exit()

s.listen(1)
print('listening on', addr)

onCol = '#26a1ff'
offCol = '#003366'
col1_strs = (onCol, offCol, offCol, offCol, offCol)
col2_strs = (onCol, offCol, offCol, offCol, offCol)
col3_strs = (onCol, offCol, offCol, offCol, offCol)
colall_strs = col1_strs + col2_strs + col3_strs
buttonValMax = 4
# Listen for connections, serve client
while True:
    try:
        cl, addr = s.accept()
        #print('client connected from', addr)
        request = cl.recv(1024)
        #print("request:")
        #print(request)
        request = str(request)
        col1_0_on = request.find('col1_0=on')
        col1_1_on = request.find('col1_1=on')
        col1_2_on = request.find('col1_2=on')
        col1_3_on = request.find('col1_3=on')
        col1_4_on = request.find('col1_4=on')
        col2_0_on = request.find('col2_0=on')
        col2_1_on = request.find('col2_1=on')
        col2_2_on = request.find('col2_2=on')
        col2_3_on = request.find('col2_3=on')
        col2_4_on = request.find('col2_4=on')
        col3_0_on = request.find('col3_0=on')
        col3_1_on = request.find('col3_1=on')
        col3_2_on = request.find('col3_2=on')
        col3_3_on = request.find('col3_3=on')
        col3_4_on = request.find('col3_4=on')

        if col1_0_on == 8:
            col1_strs = (onCol, offCol, offCol, offCol, offCol)
            duty = 0
        if col1_1_on == 8:
            col1_strs = (offCol, onCol, offCol, offCol, offCol)
            duty = 1
        if col1_2_on == 8:
            col1_strs = (offCol, offCol, onCol, offCol, offCol)
            duty = 2
        if col1_3_on == 8:
            col1_strs = (offCol, offCol, offCol, onCol, offCol)
            duty = 3
        if col1_4_on == 8:
            col1_strs = (offCol, offCol, offCol, offCol, onCol)
            duty = 4

        if col2_0_on == 8:
            col2_strs = (onCol, offCol, offCol, offCol, offCol)
            duty = 0
        if col2_1_on == 8:
            col2_strs = (offCol, onCol, offCol, offCol, offCol)
            duty = 1
        if col2_2_on == 8:
            col2_strs = (offCol, offCol, onCol, offCol, offCol)
            duty = 2
        if col2_3_on == 8:
            col2_strs = (offCol, offCol, offCol, onCol, offCol)
            duty = 3
        if col2_4_on == 8:
            col2_strs = (offCol, offCol, offCol, offCol, onCol)
            duty = 4

        if col3_0_on == 8:
            col3_strs = (onCol, offCol, offCol, offCol, offCol)
            duty = 0
        if col3_1_on == 8:
            col3_strs = (offCol, onCol, offCol, offCol, offCol)
            duty = 1
        if col3_2_on == 8:
            col3_strs = (offCol, offCol, onCol, offCol, offCol)
            duty = 2
        if col3_3_on == 8:
            col3_strs = (offCol, offCol, offCol, onCol, offCol)
            duty = 3
        if col3_4_on == 8:
            col3_strs = (offCol, offCol, offCol, offCol, onCol)
            duty = 4

        # アノードコモンの場合、下記の行を有効に   
        #duty = buttonValMax - duty

        if col1_0_on == 8 or col1_1_on == 8 or col1_2_on == 8 or col1_3_on == 8 or col1_4_on == 8:
            pwm1.duty_u16(int(duty*65535/buttonValMax))
        if col2_0_on == 8 or col2_1_on == 8 or col2_2_on == 8 or col2_3_on == 8 or col2_4_on == 8:
            pwm2.duty_u16(int(duty*65535/buttonValMax))      
        if col3_0_on == 8 or col3_1_on == 8 or col3_2_on == 8 or col3_3_on == 8 or col3_4_on == 8:
            pwm3.duty_u16(int(duty*65535/buttonValMax))

        colall_strs = col1_strs + col2_strs + col3_strs

        response = html % colall_strs 
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        print('connection closed')

    except KeyboardInterrupt as e:
        break

s.close()
s = None
なお、「1. ブラウザのボタンによるLEDの点灯」で述べたように、YOUR_WIFI_SSID と YOUR_WIFI_PASSWORD を自分の環境のものに変更することと、 LCD なしでプログラムを実行したい場合は「# for LCD ~ # End of LCD」の部分の差し替えが必要ですのでご注意ください。

さらに、プログラムの書き換え時は、Pico W 上で動作しているプログラムを Thonny の「STOP (停止)」ボタンから一旦停止する必要があることもご注意ください。

以上のプログラムを実行じ、ブラウザで LCD 上のアドレスにアクセスすると、下記のようなページが開きます。スライダの代わりに、ボタンによってRGBフルカラーLEDの3つの色の明るさを調整する仕組みです。


4. ブラウザのボタンによる DC モーターの速度制御

PCやスマートフォンのブラウザで、Pico W に接続された DC モーターの回転速度を制御する演習を行ってみましょう。9.6 章の内容です。

なお、Rapsberry Pi を対象とした本書では、DC モーターの速度制御に、ブラウザのタッチイベントを用いました。 しかし、Pico W ではタッチイベントを用いるのは難しそうに思えましたので(専門的に言えば、JavaScript で取得したタッチイベントの情報を回路に反映する方法が思いつかない)、簡易的にボタンによる制御を行うことにしました。

必要な回路は下図の通りです。
import time
import sys
import network
import socket
from time import sleep
from machine import Pin, PWM, I2C

pwm1 = PWM(Pin(16))
pwm2 = PWM(Pin(17))
pwm1.freq(100)
pwm2.freq(100)
pwm1.duty_u16(0)
pwm2.duty_u16(0)

i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
i2c.scan()

ssid = 'YOUR_WIFI_SSID'
password = 'YOUR_WIFI_PASSWORD'

##### for LCD
def setup_st7032():
    c_lower = (contrast & 0xf)
    c_upper = (contrast & 0x30)>>4
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x39, 0x14, 0x70|c_lower, 0x54|c_upper, 0x6c]))
    sleep(0.2)
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x0d, 0x01]))
    sleep(0.001)

def clear():
    global position
    global line
    position = 0
    line = 0
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x01]))
    sleep(0.001)

def newline():
    global position
    global line
    if line == display_lines-1:
        clear()
    else:
        line += 1
        position = chars_per_line*line
        i2c.writeto_mem(address_st7032, register_setting, bytes([0xc0]))
        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()
    i2c.writeto_mem(address_st7032, register_display, bytes([byte_data]))
    position += 1

def check_writable(c):
    if c >= 0x06 and c <= 0xff :
        return c
    else:
        return 0x20 # 空白文字

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()
##### end of LCD

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

html = """<!DOCTYPE html><html>
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>4 - ボタンによるDCモーターの速度制御</title>
<style>button {
    width: auto;
    height: auto;
    background: #003366;
    font-weight: normal;
    font-size: 14pt;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 14px;
    color: #ffffff;
    padding: 10px 20px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: 1px solid #003366;
    -moz-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    -webkit-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    text-shadow:
        0px -1px 0px rgba(000,000,000,0.7),
        0px 1px 0px rgba(255,255,255,0.3);
}
</style></head>
<p>押したボタンにより、モーターが回転を開始します。中央のボタンをクリックしないとモーターは止まりません。</p>
<div align="center">
<form><button name="vel_n2" value="on" type="submit" style="background: %s;"> -2 </button>
<button name="vel_n1" value="on" type="submit" style="background: %s;"> -1 </button>
<button name="vel_0" value="on" type="submit" style="background: %s;"> 0 </button>
<button name="vel_p1" value="on" type="submit" style="background: %s;"> +1 </button>
<button name="vel_p2" value="on" type="submit" style="background: %s;"> +2 </button>
</form>
<br /><br />
</div>
</body></html>
"""

# Wait for connect or fail
max_wait = 20
while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    max_wait -= 1
    print('waiting for connection...')
    write_string('waiting for conn') #showing on LCD
    time.sleep(1)

# Handle connection error
if wlan.status() != 3:
    write_string('conn. failed') # showing on LCD
    raise RuntimeError('network connection failed')
else:
    print('Connected')
    status = wlan.ifconfig()
    print( 'ip = ' + status[0] )
    write_string(status[0]) #showing on LCD

# Open socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(addr)
except OSError as e:
    print(e)
    s.close()
    s = None
    write_string('socket error') #showing on LCD
    sys.exit()

s.listen(1)
print('listening on', addr)

onCol = '#26a1ff'
offCol = '#003366'
vel_strs = (offCol, offCol, onCol, offCol, offCol)

duty_max = 0.7

# Listen for connections, serve client
while True:
    try:
        cl, addr = s.accept()
        #print('client connected from', addr)
        request = cl.recv(1024)
        #print("request:")
        #print(request)
        request = str(request)
        vel_n2_on = request.find('vel_n2=on')
        vel_n1_on = request.find('vel_n1=on')
        vel_0_on = request.find('vel_0=on')
        vel_p1_on = request.find('vel_p1=on')
        vel_p2_on = request.find('vel_p2=on')

        if vel_n2_on == 8:
            vel_strs = (onCol, offCol, offCol, offCol, offCol)
            pwm2.duty_u16(0)
            pwm1.duty_u16(int(65535*duty_max))
        if vel_n1_on == 8:
            vel_strs = (offCol, onCol, offCol, offCol, offCol)
            pwm2.duty_u16(0)
            pwm1.duty_u16(int(65535*duty_max/2))
        if vel_0_on == 8:
            vel_strs = (offCol, offCol, onCol, offCol, offCol)
            pwm1.duty_u16(0)
            pwm2.duty_u16(0)
        if vel_p1_on == 8:
            vel_strs = (offCol, offCol, offCol, onCol, offCol)
            pwm1.duty_u16(0)
            pwm2.duty_u16(int(65535*duty_max/2))
        if vel_p2_on == 8:
            vel_strs = (offCol, offCol, offCol, offCol, onCol)
            pwm1.duty_u16(0)
            pwm2.duty_u16(int(65535*duty_max))

        response = html % vel_strs 
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        print('connection closed')

    except KeyboardInterrupt as e:
        break

s.close()
s = None
なお、「1. ブラウザのボタンによるLEDの点灯」で述べたように、YOUR_WIFI_SSID と YOUR_WIFI_PASSWORD を自分の環境のものに変更することと、 LCD なしでプログラムを実行したい場合は「# for LCD ~ # End of LCD」の部分の差し替えが必要ですのでご注意ください。

さらに、プログラムの書き換え時は、Pico W 上で動作しているプログラムを Thonny の「STOP (停止)」ボタンから一旦停止する必要があることもご注意ください。

以上のプログラムを実行し、LCD上のアドレスにブラウザでアクセスすると、下記のようなページが開きます。タッチの代わりに、ボタンでモーターの回転の向きと速度を調整する仕組みです。


5. ブラウザのボタンによるサーボモーターの角度制御

PCやスマートフォンのブラウザで、Pico W に接続されたサーボモーターの角度を制御する演習を行ってみましょう。9.7 章(付録 PDF)の内容です。

なお、Rapsberry Pi を対象とした本書では、サーボモーターの角度制御に、ブラウザのスライダを用いました。 しかし、Pico W ではスライダを用いるのは難しそうに思えましたので(専門的に言えば、JavaScript で取得したスライダの値を回路に反映する方法が思いつかない)、簡易的にボタンによる制御を行うことにしました。

必要な回路は下図の通りです。
import time
import sys
import network
import socket
from time import sleep
from machine import Pin, PWM, I2C

def servo_duty_hwpwm_web(val):
    val_min = 0
    val_max = 4
    servo_min = 0.035   # 最小デューティ比3.5%
    servo_max = 0.1  # 最大デューティ比10%
    if val < val_min:
        val = val_min
    duty = (servo_min-servo_max)*(val-val_min)/(val_max-val_min) + servo_max
    # 一般的なサーボモーターはこちらを有効に
    #duty = (servo_max-servo_min)*(val-val_min)/(val_max-val_min) + servo_min
    return duty

pwm1 = PWM(Pin(16))
pwm1.freq(50)
pwm1.duty_u16(int(65535*servo_duty_hwpwm_web(2)))

i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
i2c.scan()

ssid = 'YOUR_WIFI_SSID'
password = 'YOUR_WIFI_PASSWORD'

##### for LCD
def setup_st7032():
    c_lower = (contrast & 0xf)
    c_upper = (contrast & 0x30)>>4
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x39, 0x14, 0x70|c_lower, 0x54|c_upper, 0x6c]))
    sleep(0.2)
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x0d, 0x01]))
    sleep(0.001)

def clear():
    global position
    global line
    position = 0
    line = 0
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x01]))
    sleep(0.001)

def newline():
    global position
    global line
    if line == display_lines-1:
        clear()
    else:
        line += 1
        position = chars_per_line*line
        i2c.writeto_mem(address_st7032, register_setting, bytes([0xc0]))
        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()
    i2c.writeto_mem(address_st7032, register_display, bytes([byte_data]))
    position += 1

def check_writable(c):
    if c >= 0x06 and c <= 0xff :
        return c
    else:
        return 0x20 # 空白文字

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()
##### end of LCD

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

html = """<!DOCTYPE html><html>
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>7 - サーボモーターの制御</title>
<style>button {
    width: auto;
    height: auto;
    background: #003366;
    font-weight: normal;
    font-size: 14pt;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 14px;
    color: #ffffff;
    padding: 10px 20px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: 1px solid #003366;
    -moz-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    -webkit-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    text-shadow:
        0px -1px 0px rgba(000,000,000,0.7),
        0px 1px 0px rgba(255,255,255,0.3);
}
</style></head>
<p>押したボタンにより、サーボモーターの位置が決まります。</p>
<div align="center">
<form><button name="pos0" value="on" type="submit" style="background: %s;"> 0 </button>
<button name="pos1" value="on" type="submit" style="background: %s;"> 1 </button>
<button name="pos2" value="on" type="submit" style="background: %s;"> 2 </button>
<button name="pos3" value="on" type="submit" style="background: %s;"> 3 </button>
<button name="pos4" value="on" type="submit" style="background: %s;"> 4 </button>
</form>
<br /><br />
</div>
</body></html>
"""

# Wait for connect or fail
max_wait = 20
while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    max_wait -= 1
    print('waiting for connection...')
    write_string('waiting for conn') #showing on LCD
    time.sleep(1)

# Handle connection error
if wlan.status() != 3:
    write_string('conn. failed') # showing on LCD
    raise RuntimeError('network connection failed')
else:
    print('Connected')
    status = wlan.ifconfig()
    print( 'ip = ' + status[0] )
    write_string(status[0]) #showing on LCD

# Open socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(addr)
except OSError as e:
    print(e)
    s.close()
    s = None
    write_string('socket error') #showing on LCD
    sys.exit()

s.listen(1)
print('listening on', addr)

onCol = '#26a1ff'
offCol = '#003366'
pos_strs = (offCol, offCol, onCol, offCol, offCol)

# Listen for connections, serve client
while True:
    try:
        cl, addr = s.accept()
        #print('client connected from', addr)
        request = cl.recv(1024)
        #print("request:")
        #print(request)
        request = str(request)
        pos0_on = request.find('pos0=on')
        pos1_on = request.find('pos1=on')
        pos2_on = request.find('pos2=on')
        pos3_on = request.find('pos3=on')
        pos4_on = request.find('pos4=on')

        if pos0_on == 8:
            pos_strs = (onCol, offCol, offCol, offCol, offCol)
            pwm1.duty_u16(int(65535*servo_duty_hwpwm_web(0)))
        if pos1_on == 8:
            pos_strs = (offCol, onCol, offCol, offCol, offCol)
            pwm1.duty_u16(int(65535*servo_duty_hwpwm_web(1)))
        if pos2_on == 8:
            pos_strs = (offCol, offCol, onCol, offCol, offCol)
            pwm1.duty_u16(int(65535*servo_duty_hwpwm_web(2)))
        if pos3_on == 8:
            pos_strs = (offCol, offCol, offCol, onCol, offCol)
            pwm1.duty_u16(int(65535*servo_duty_hwpwm_web(3)))
        if pos4_on == 8:
            pos_strs = (offCol, offCol, offCol, offCol, onCol)
            pwm1.duty_u16(int(65535*servo_duty_hwpwm_web(4)))

        response = html % pos_strs 
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        print('connection closed')

    except KeyboardInterrupt as e:
        break

s.close()
s = None
なお、「1. ブラウザのボタンによるLEDの点灯」で述べたように、YOUR_WIFI_SSID と YOUR_WIFI_PASSWORD を自分の環境のものに変更することと、 LCD なしでプログラムを実行したい場合は「# for LCD ~ # End of LCD」の部分の差し替えが必要ですのでご注意ください。

さらに、プログラムの書き換え時は、Pico W 上で動作しているプログラムを Thonny の「STOP (停止)」ボタンから一旦停止する必要があることもご注意ください。

以上のプログラムを実行し、LCD上のアドレスにブラウザでアクセスすると、下記のようなページが開きます。スライダの代わりに、ボタンでサーボモーターの位置を調整する仕組みです。


6. ブラウザのボタンによるキャタピラ式模型の制御

PCやスマートフォンのブラウザで、Pico W に接続されたキャタピラ式模型を制御する演習を行ってみましょう。10 章の内容です。

ここまでの演習同様、「前進」、「後退」、「右旋回」、「左旋回」、「静止」という 5 つのボタンによる簡易的な制御ですのでご了承ください。

import time
import sys
import network
import socket
from time import sleep
from machine import Pin, PWM, I2C

pwm1 = PWM(Pin(16))
pwm2 = PWM(Pin(17))
pwm3 = PWM(Pin(18))
pwm4 = PWM(Pin(19))
pwm1.freq(100)
pwm2.freq(100)
pwm3.freq(100)
pwm4.freq(100)
pwm1.duty_u16(0)
pwm2.duty_u16(0)
pwm3.duty_u16(0)
pwm4.duty_u16(0)

i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
i2c.scan()

ssid = 'YOUR_WIFI_SSID'
password = 'YOUR_WIFI_PASSWORD'

##### for LCD
def setup_st7032():
    c_lower = (contrast & 0xf)
    c_upper = (contrast & 0x30)>>4
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x39, 0x14, 0x70|c_lower, 0x54|c_upper, 0x6c]))
    sleep(0.2)
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x38, 0x0d, 0x01]))
    sleep(0.001)

def clear():
    global position
    global line
    position = 0
    line = 0
    i2c.writeto_mem(address_st7032, register_setting, bytes([0x01]))
    sleep(0.001)

def newline():
    global position
    global line
    if line == display_lines-1:
        clear()
    else:
        line += 1
        position = chars_per_line*line
        i2c.writeto_mem(address_st7032, register_setting, bytes([0xc0]))
        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()
    i2c.writeto_mem(address_st7032, register_display, bytes([byte_data]))
    position += 1

def check_writable(c):
    if c >= 0x06 and c <= 0xff :
        return c
    else:
        return 0x20 # 空白文字

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()
##### end of LCD

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

html = """<!DOCTYPE html><html>
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>5 - クローラーの操作</title>
<style>button {
    width: auto;
    height: auto;
    background: #003366;
    font-weight: normal;
    font-size: 14pt;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 14px;
    color: #ffffff;
    padding: 10px 20px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: 1px solid #003366;
    -moz-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    -webkit-box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    box-shadow:
        0px 1px 3px rgba(000,000,000,0.5),
        inset 0px 0px 1px rgba(255,255,255,0.5);
    text-shadow:
        0px -1px 0px rgba(000,000,000,0.7),
        0px 1px 0px rgba(255,255,255,0.3);
}
</style></head>
<p>押したボタンにより、クローラーが移動します。中央のボタンをクリックしないとクローラーは止まりません。</p>
<div align="center">
<table border="0">
<tr align="center">
<td></td>
<td><form><button name="forward" value="on" type="submit" style="background: %s;"> ↑ </button></form></td>
<td></td>
</tr>
<tr align="center">
<td><form><button name="rot_l" value="on" type="submit" style="background: %s;"> ← </button></form></td>
<td><form><button name="stop" value="on" type="submit" style="background: %s;"> 〇 </button></form></td>
<td><form><button name="rot_r" value="on" type="submit" style="background: %s;"> → </button></form></td>
</tr>
<tr align="center">
<td></td>
<td><form><button name="backward" value="on" type="submit" style="background: %s;"> ↓ </button></form></td>
<td></td>
</tr>
</table>
<br /><br />
</div>
</body></html>
"""

# Wait for connect or fail
max_wait = 20
while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    max_wait -= 1
    print('waiting for connection...')
    write_string('waiting for conn') #showing on LCD
    time.sleep(1)

# Handle connection error
if wlan.status() != 3:
    write_string('conn. failed') # showing on LCD
    raise RuntimeError('network connection failed')
else:
    print('Connected')
    status = wlan.ifconfig()
    print( 'ip = ' + status[0] )
    write_string(status[0]) #showing on LCD

# Open socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(addr)
except OSError as e:
    print(e)
    s.close()
    s = None
    write_string('socket error') #showing on LCD
    sys.exit()

s.listen(1)
print('listening on', addr)

onCol = '#26a1ff'
offCol = '#003366'
move_strs = (offCol, offCol, onCol, offCol, offCol)

duty_max = 0.7

# Listen for connections, serve client
while True:
    try:
        cl, addr = s.accept()
        #print('client connected from', addr)
        request = cl.recv(1024)
        #print("request:")
        #print(request)
        request = str(request)
        forward_on = request.find('forward=on')
        rot_l_on = request.find('rot_l=on')
        stop_on = request.find('stop=on')
        rot_r_on = request.find('rot_r=on')
        backward_on = request.find('backward=on')

        if forward_on == 8:
            move_strs = (onCol, offCol, offCol, offCol, offCol)
            pwm2.duty_u16(0)
            pwm1.duty_u16(int(65535*duty_max))
            pwm4.duty_u16(0)
            pwm3.duty_u16(int(65535*duty_max))
        if rot_l_on == 8:
            move_strs = (offCol, onCol, offCol, offCol, offCol)
            pwm1.duty_u16(0)
            pwm2.duty_u16(int(65535*duty_max))
            pwm4.duty_u16(0)
            pwm3.duty_u16(int(65535*duty_max))
        if stop_on == 8:
            move_strs = (offCol, offCol, onCol, offCol, offCol)
            pwm1.duty_u16(0)
            pwm2.duty_u16(0)
            pwm3.duty_u16(0)
            pwm4.duty_u16(0)
        if rot_r_on == 8:
            move_strs = (offCol, offCol, offCol, onCol, offCol)
            pwm2.duty_u16(0)
            pwm1.duty_u16(int(65535*duty_max))
            pwm3.duty_u16(0)
            pwm4.duty_u16(int(65535*duty_max))
        if backward_on == 8:
            move_strs = (offCol, offCol, offCol, offCol, onCol)
            pwm1.duty_u16(0)
            pwm2.duty_u16(int(65535*duty_max))
            pwm3.duty_u16(0)
            pwm4.duty_u16(int(65535*duty_max))

        response = html % move_strs 
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        print('connection closed')

    except KeyboardInterrupt as e:
        break

s.close()
s = None
なお、「1. ブラウザのボタンによるLEDの点灯」で述べたように、YOUR_WIFI_SSID と YOUR_WIFI_PASSWORD を自分の環境のものに変更することと、 LCD なしでプログラムを実行したい場合は「# for LCD ~ # End of LCD」の部分の差し替えが必要ですのでご注意ください。

さらに、プログラムの書き換え時は、Pico W 上で動作しているプログラムを Thonny から一旦停止する必要があることもご注意ください。

以上のプログラムを実行し、LCD上のアドレスにブラウザでアクセスすると、下記のようなページが開きます。タッチの代わりに、ボタンでキャタピラ式模型の移動方法を変更する仕組みです。