そうか、PHPでもモノにつながるな

投稿日:

raspi_index Raspberry Piは要するにLinuxマシンなので、ウェブサーバも選り取り見取りだ。単にウェブインターフェイスを備えたモノのプロトをするならArduinoのほうが手っ取り早いが、いかんせん、リッチなウェブ・コンテンツをArduino+ETHERNET SHIELD 2でサービスするのは難しく、不可能ではないにもせよ、ウェブ・コンテンツに写真を置くなどというのはほとんど無理であった。

 Raspberry Piはコンピュータ・パワーが大きいから、大きなウェブ・コンテンツも本体内に飲み込んでしまえる。

 そこで、である。Raspberry Piで遊ぶのに、なにも世間の教科書通りにPythonやRubyの作例を打ち込むだけが能ではない。CだってC++だって、PerlだってPHPだって動くはずである。SMTPもPOPもNTPも、なんだって動く。多分MySQLだのPostgreSQLだのも動くだろう。

 Apacheをインストールして動かしてみよう。それから、PHPでGPIOを突っついてみよう。これは、PHPでハードウェアを動かすことにつながる。無駄に贅沢なWebインターフェイスでLチカ、というのも面白い。

Apacheのインストールに先立ち、今動いているサービスを確かめよう

 まず、今起動しているモノを確認してみよう。RedHat系のLinux、Fedoraなどはこういう時に「chkconfig –list」などとするが、Raspberry Piに入っているのは「Raspbian」だ。RaspbianはDebian系のディストリビューションなので、chkconfigはない。

 私はUNIX育ちのオッサンで、SVR4.2の管理人を長年やっきた。Linuxに関してはフリー時代のRedHat(4~9ぐらいまで)、それ以降はずっとFedora Coreを使ってきたので、Debianは良く知らないのである。ま、これも機会だから、ちょっと体験してみようではないの。

 ネット情報によると「insserv」「update-rc.d」「sysv-rc-conf」「rcconf」「upstart」などがあるという。

# insserv -s
K:01:0 1 6:triggerhappy
K:07:0 6:umountfs
K:04:0 6:umountnfs.sh
K:02:0 6:sendsigs
K:01:0 6:plymouth
K:03:0 1 6:rsyslog
K:06:0 6 S:hwclock.sh
K:01:0 1 6:alsa-utils
K:06:0 6:networking
K:05:0 1 2 3 4 5 6 S:rpcbind
K:05:0 1 2 3 4 5 6 S:nfs-common
K:01:0 6:urandom
K:01:0 1 6:avahi-daemon
K:01:0 1 6:dhcpcd
K:01:0 1 6:cgroup-bin
K:08:0 6:umountroot
K:09:0:halt
K:09:6:reboot
K:01:0 1 6:fake-hwclock
K:01:0 1 2 6:lightdm
K:01:0 1 6:ifplugd
S:02:S:udev
S:03:S:keyboard-setup
S:15:S:console-setup
S:01:2 3 4 5:triggerhappy
S:08:S:mountall.sh
S:09:S:mountall-bootclean.sh
S:12:S:mountnfs.sh
S:13:S:mountnfs-bootclean.sh
S:04:2 3 4 5:plymouth
S:01:2 3 4 5:rsyslog
S:16:S:alsa-utils
S:11:S:networking
S:10:S:urandom
S:04:S:mountdevsubfs.sh
S:05:S:checkroot.sh
S:03:2 3 4 5:avahi-daemon
S:02:2 3 4 5:dbus
S:01:2 3 4 5:dhcpcd
S:01:2 3 4 5:cgroup-bin
S:01:S:mountkernfs.sh
S:01:S:fake-hwclock
S:03:3 4 5:lightdm
S:16:S:x11-common
S:14:S:kbd
S:01:2 3 4 5:ifplugd
S:02:1:single
S:01:1:killprocs
S:01:1 2 3 4 5:bootlogs
S:01:S:hostname.sh
S:01:1 2 3 4 5:motd
S:02:2 3 4 5:dphys-swapfile
S:04:2 3 4 5:rc.local
S:04:2 3 4 5:rmnologin
S:02:2 3 4 5:cron
S:02:2 3 4 5:rsync
S:02:2 3 4 5:ssh
S:02:2 3 4 5:ntp
S:01:2 3 4 5:sudo
S:16:S:raspi-config
S:10:S:udev-mtab
S:06:S:checkroot-bootclean.sh
S:16:S:bootmisc.sh
S:06:S:kmod
S:16:S:plymouth-log
S:07:S:checkfs.sh
S:06:S:mtab.sh
S:10:S:procps

 ほほー、なるほど、コレは多分、左から現状(StartかKillか)、起動順、起動すべきRun-Levelのリスト、サービス名、だろうなあ。はて、S、Kじゃないほうの、Run-Levelの「S」は、なんだろ、と調べると、「起動順で一番はじめ、かつ、どのRun-Levelでも共通で起動」ということのようである。

 で、

# insserv -s | egrep '(pache)|(ttp)'
#

 なんてことをやっても、全然何も出ないから、デフォルトのRaspbianではhttpらしきものやApacheらしきものは何も動いていない。

Apacheをインストール

 それでは、というわけで、

# apt-get -s install apache2
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
以下の特別パッケージがインストールされます:
  apache2-mpm-worker apache2-utils apache2.2-bin apache2.2-common libapr1 libaprutil1
  libaprutil1-dbd-sqlite3 libaprutil1-ldap ssl-cert
提案パッケージ:
  apache2-doc apache2-suexec apache2-suexec-custom openssl-blacklist
以下のパッケージが新たにインストールされます:
  apache2 apache2-mpm-worker apache2-utils apache2.2-bin apache2.2-common libapr1
  libaprutil1 libaprutil1-dbd-sqlite3 libaprutil1-ldap ssl-cert
アップグレード: 0 個、新規インストール: 10 個、削除: 0 個、保留: 0 個。
Inst libapr1 (1.4.6-3+deb7u1 Raspbian:7.0/oldstable [armhf])
Inst libaprutil1 (1.4.1-3 Raspbian:7.0/oldstable [armhf])
Inst libaprutil1-dbd-sqlite3 (1.4.1-3 Raspbian:7.0/oldstable [armhf])
Inst libaprutil1-ldap (1.4.1-3 Raspbian:7.0/oldstable [armhf])
Inst apache2.2-bin (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Inst apache2-utils (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Inst apache2.2-common (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Inst apache2-mpm-worker (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Inst apache2 (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Inst ssl-cert (1.0.32 Raspbian:7.0/oldstable [all])
Conf libapr1 (1.4.6-3+deb7u1 Raspbian:7.0/oldstable [armhf])
Conf libaprutil1 (1.4.1-3 Raspbian:7.0/oldstable [armhf])
Conf libaprutil1-dbd-sqlite3 (1.4.1-3 Raspbian:7.0/oldstable [armhf])
Conf libaprutil1-ldap (1.4.1-3 Raspbian:7.0/oldstable [armhf])
Conf apache2.2-bin (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Conf apache2-utils (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Conf apache2.2-common (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Conf apache2-mpm-worker (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Conf apache2 (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Conf ssl-cert (1.0.32 Raspbian:7.0/oldstable [all])
#

 特に問題なさそうだな、というわけで……

# apt-get install apache2
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
以下の特別パッケージがインストールされます:
  apache2-mpm-worker apache2-utils apache2.2-bin apache2.2-common libapr1 libaprutil1
  libaprutil1-dbd-sqlite3 libaprutil1-ldap ssl-cert
提案パッケージ:
  apache2-doc apache2-suexec apache2-suexec-custom openssl-blacklist
以下のパッケージが新たにインストールされます:
  apache2 apache2-mpm-worker apache2-utils apache2.2-bin apache2.2-common libapr1
  libaprutil1 libaprutil1-dbd-sqlite3 libaprutil1-ldap ssl-cert
アップグレード: 0 個、新規インストール: 10 個、削除: 0 個、保留: 0 個。
1,356 kB のアーカイブを取得する必要があります。
この操作後に追加で 4,585 kB のディスク容量が消費されます。
続行しますか [Y/n]? y
取得:1 http://mirrordirector.raspbian.org/raspbian/ wheezy/main libapr1 armhf 1.4.6-3+deb7u1 [90.9 kB]
取得:2 http://mirrordirector.raspbian.org/raspbian/ wheezy/main libaprutil1 armhf 1.4.1-3 [77.1 kB]
取得:3 http://mirrordirector.raspbian.org/raspbian/ wheezy/main libaprutil1-dbd-sqlite3 armhf 1.4.1-3 [17.2 kB]
取得:4 http://mirrordirector.raspbian.org/raspbian/ wheezy/main libaprutil1-ldap armhf 1.4.1-3 [16.0 kB]
取得:5 http://mirrordirector.raspbian.org/raspbian/ wheezy/main apache2.2-bin armhf 2.2.22-13+deb7u6 [676 kB]
取得:6 http://mirrordirector.raspbian.org/raspbian/ wheezy/main apache2-utils armhf 2.2.22-13+deb7u6 [163 kB]
取得:7 http://mirrordirector.raspbian.org/raspbian/ wheezy/main apache2.2-common armhf 2.2.22-13+deb7u6 [292 kB]
取得:8 http://mirrordirector.raspbian.org/raspbian/ wheezy/main apache2-mpm-worker armhf 2.2.22-13+deb7u6 [2,238 B]
取得:9 http://mirrordirector.raspbian.org/raspbian/ wheezy/main apache2 armhf 2.2.22-13+deb7u6 [1,440 B]
取得:10 http://mirrordirector.raspbian.org/raspbian/ wheezy/main ssl-cert all 1.0.32 [19.5 kB]
1,356 kB を 4秒 で取得しました (281 kB/s)
パッケージを事前設定しています ...
以前に未選択のパッケージ libapr1 を選択しています。
(データベースを読み込んでいます ... 現在 78616 個のファイルとディレクトリがインストールされています。)
(.../libapr1_1.4.6-3+deb7u1_armhf.deb から) libapr1 を展開しています...
以前に未選択のパッケージ libaprutil1 を選択しています。
(.../libaprutil1_1.4.1-3_armhf.deb から) libaprutil1 を展開しています...
以前に未選択のパッケージ libaprutil1-dbd-sqlite3 を選択しています。
(.../libaprutil1-dbd-sqlite3_1.4.1-3_armhf.deb から) libaprutil1-dbd-sqlite3 を展開しています...
以前に未選択のパッケージ libaprutil1-ldap を選択しています。
(.../libaprutil1-ldap_1.4.1-3_armhf.deb から) libaprutil1-ldap を展開しています...
以前に未選択のパッケージ apache2.2-bin を選択しています。
(.../apache2.2-bin_2.2.22-13+deb7u6_armhf.deb から) apache2.2-bin を展開しています...
以前に未選択のパッケージ apache2-utils を選択しています。
(.../apache2-utils_2.2.22-13+deb7u6_armhf.deb から) apache2-utils を展開しています...
以前に未選択のパッケージ apache2.2-common を選択しています。
(.../apache2.2-common_2.2.22-13+deb7u6_armhf.deb から) apache2.2-common を展開しています...
以前に未選択のパッケージ apache2-mpm-worker を選択しています。
(.../apache2-mpm-worker_2.2.22-13+deb7u6_armhf.deb から) apache2-mpm-worker を展開していま す...
以前に未選択のパッケージ apache2 を選択しています。
(.../apache2_2.2.22-13+deb7u6_armhf.deb から) apache2 を展開しています...
以前に未選択のパッケージ ssl-cert を選択しています。
(.../ssl-cert_1.0.32_all.deb から) ssl-cert を展開しています...
man-db のトリガを処理しています ...
libapr1 (1.4.6-3+deb7u1) を設定しています ...
libaprutil1 (1.4.1-3) を設定しています ...
libaprutil1-dbd-sqlite3 (1.4.1-3) を設定しています ...
libaprutil1-ldap (1.4.1-3) を設定しています ...
apache2.2-bin (2.2.22-13+deb7u6) を設定しています ...
apache2-utils (2.2.22-13+deb7u6) を設定しています ...
apache2.2-common (2.2.22-13+deb7u6) を設定しています ...
Enabling site default.
Enabling module alias.
Enabling module autoindex.
Enabling module dir.
Enabling module env.
Enabling module mime.
Enabling module negotiation.
Enabling module setenvif.
Enabling module status.
Enabling module auth_basic.
Enabling module deflate.
Enabling module authz_default.
Enabling module authz_user.
Enabling module authz_groupfile.
Enabling module authn_file.
Enabling module authz_host.
Enabling module reqtimeout.
apache2-mpm-worker (2.2.22-13+deb7u6) を設定しています ...
[....] Starting web server: apache2apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName
. ok
apache2 (2.2.22-13+deb7u6) を設定しています ...
ssl-cert (1.0.32) を設定しています ...
#

 なんてことなく終わる。

Apacheの起動状況を確かめる

 次に、

# insserv -s | egrep '(pache)|(ttp)'
K:01:0 1 6:apache2
S:02:2 3 4 5:apache2
#

……ぬぅ、StartなのかKillなのか、どっちやねん、というわけで、ブラウザにアドレスを入れてみると、

raspi-apache2start

……と、普通にApacheは動いている。KとS両方出るのは、起動順が01、02となっていることから、Run-Level 0、1、6ではどうあろうと強制終了し、改めて2、3、4、5では起動、ということであろうか。

 えーっと、自動起動のほうはこれでいいから、サービスの起動停止のテストは……、と。

# /etc/init.d/apache2 stop
[....] Stopping web server: apache2apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName
[ ok waiting ..
root@satoraspi:~# /etc/init.d/apache2 start
[....] Starting web server: apache2apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName
. ok
#

 ふむ、だいだい動いとる。

ドキュメントルートのindex.htmlを作る

 とりあえず、indexだけ、かっこよくしておこう。えーっと、ドキュメントルートはどこかいな、と。

# ls -Fla /etc/ | egrep '(http)|(apache)'
drwxr-xr-x   7 root root    4096  8月 30 09:01 apache2/
root@satoraspi:~# ls /etc/apache2
apache2.conf  envvars  mods-available  ports.conf       sites-enabled
conf.d        magic    mods-enabled    sites-available
# cd /etc/apache2
# grep 'DocumentRoot' */*
sites-available/default:        DocumentRoot /var/www
sites-available/default-ssl:    DocumentRoot /var/www
sites-enabled/000-default:      DocumentRoot /var/www

 ……と、いうわけで普通に「/var/www」の下じゃのう。普通のユーザに戻って、

toshio@satoraspi:~$ cd /var/www
toshio@satoraspi:/var/www$ ls -Fla
合計 12
drwxr-xr-x  2 root root 4096  8月 30 09:01 ./
drwxr-xr-x 12 root root 4096  8月 30 09:01 ../
-rw-r--r--  1 root root  177  8月 30 09:01 index.html
toshio@satoraspi:/var/www$ cat index.html
<html><body><h1>It works!</h1>
<p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p>
</body></html>toshio@satoraspi:/var/www$

 はあ、さっきブラウザに出てたヤツでんな、と。

 一応残しておいて……

toshio@satoraspi:/var/www$ sudo cp index.html .index.html.ORG
[sudo] password for toshio:
toshio@satoraspi:/var/www$ sudo chown toshio:pi index.html
toshio@satoraspi:/var/www$ ls -Fla
合計 16
drwxr-xr-x  2 root   root 4096  8月 30 09:37 ./
drwxr-xr-x 12 root   root 4096  8月 30 09:01 ../
-rw-r--r--  1 root   root  177  8月 30 09:37 .index.html.ORG
-rw-r--r--  1 toshio pi    177  8月 30 09:01 index.html
toshio@satoraspi:/var/www$cd ..
toshio@satoraspi:/var$sudo chown toshio:pi www
toshio@satoraspi:/var$cd www
toshio@satoraspi:/var/www$vi index.html

 んで、ばーっ、って、書くですよ。

<html>
 <head>
  <meta name="Editor" content="vim">
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
  <link rel="icon" href="favicon.ico" type="image/x-icon" />
  <link rel="Shortcut Icon" type="image/x-icon" href="favicon.ico" />
  <title>おっさん用Raspberry Pi 2 Model B</title>
  <meta http-equiv="Keyword" content="佐藤俊夫, 佐藤, 俊夫, SATOTOSHIO, SatoToshio, sato, toshio, Raspberry Pi, Raspberry Pi 2 Model B">
 </head>
 <body bgcolor="#888888">
  <basefont size=4">
  <center>
  <table>
  <tr>
  <td>
    <h1>おっさん用Raspberry Pi 2 Model B</h1>
    <hr>
    <center><img src="raspberry_pi.png"></center>
  </td>
  </tr>
  <tr>
  <td>
    とりあえずまだ何もない。
  </td>
  </tr>
 </table>
 </center>
 </body>
</html>

 んで、画像なんかをSCPでコピーして、まずはこんなindex.htmlですな。

raspi_index

 それにしても、こんな、5千円かそこらのシングルボードでapache2のウェブサービスができるなんて、隔世の感があるな。

同じようにして、PHPをインストール

 さておき、続いてPHPを入れよう。

toshio@satoraspi:/var/www$ cd
toshio@satoraspi:~$ su -
パスワード:
root@satoraspi:~# apt-cache search php5
dwoo - PHP5 template engine
libapache2-mod-php5 - server-side, HTML-embedded scripting language (Apache 2 module)
libapache2-mod-php5filter - server-side, HTML-embedded scripting language (apache 2 filter module)
libexpect-php5 - expect module for PHP 5
libgv-php5 - PHP5 bindings for graphviz
libkohana2-modules-php - lightweight PHP5 MVC framework (extension modules)
libkohana2-php - lightweight PHP5 MVC framework
libkohana3.1-core-php - PHP5 framework core classes
libkohana3.1-php - PHP5 framework metapackage
libkohana3.2-core-php - PHP5 framework core classes
libkohana3.2-php - PHP5 framework metapackage
libow-php5 - Dallas 1-wire support: PHP5 bindings
libphp-jpgraph - Object oriented graph library for php5
libphp-jpgraph-examples - Object oriented graph library for php5 (examples)
libphp5-embed - HTML-embedded scripting language (Embedded SAPI library)
php-doc - Documentation for PHP5
php-imlib - PHP Imlib2 Extension
php5 - server-side, HTML-embedded scripting language (metapackage)
php5-adodb - Extension optimising the ADOdb database abstraction library
php5-cgi - server-side, HTML-embedded scripting language (CGI binary)
php5-cli - command-line interpreter for the php5 scripting language
php5-common - Common files for packages built from the php5 source
php5-curl - CURL module for php5
php5-dbg - Debug symbols for PHP5
php5-dev - Files for PHP5 module development
php5-enchant - Enchant module for php5
php5-exactimage - fast image manipulation library (PHP bindings)
php5-ffmpeg - audio and video support via ffmpeg for php5
php5-fpm - server-side, HTML-embedded scripting language (FPM-CGI binary)
php5-gd - GD module for php5
php5-gdcm - Grassroots DICOM PHP5 bindings
php5-geoip - GeoIP module for php5
php5-gmp - GMP module for php5
php5-imagick - ImageMagick module for php5
php5-imap - IMAP module for php5
php5-interbase - interbase/firebird module for php5
php5-intl - internationalisation module for php5
php5-lasso - Library for Liberty Alliance and SAML protocols - PHP 5 bindings
php5-ldap - LDAP module for php5
php5-librdf - PHP5 language bindings for the Redland RDF library
php5-mapscript - php5-cgi module for MapServer
php5-mcrypt - MCrypt module for php5
php5-memcache - memcache extension module for PHP5
php5-memcached - memcached extension module for PHP5, uses libmemcached
php5-midgard2 - Midgard2 Content Repository - PHP5 language bindings and module
php5-ming - Ming module for php5
php5-mysql - MySQL module for php5
php5-mysqlnd - MySQL module for php5 (Native Driver)
php5-odbc - ODBC module for php5
php5-pgsql - PostgreSQL module for php5
php5-ps - ps module for PHP 5
php5-pspell - pspell module for php5
php5-radius - PECL radius module for PHP 5
php5-recode - recode module for php5
php5-remctl - PECL module for Kerberos-authenticated command execution
php5-rrd - rrd module for PHP 5
php5-sasl - Cyrus SASL extension for PHP 5
php5-snmp - SNMP module for php5
php5-sqlite - SQLite module for php5
php5-svn - PHP Bindings for the Subversion Revision control system
php5-sybase - Sybase / MS SQL Server module for php5
php5-tidy - tidy module for php5
php5-tokyo-tyrant - PHP interface to Tokyo Cabinet's network interface, Tokyo Tyrant
php5-vtkgdcm - Grassroots DICOM VTK PHP bindings
php5-xcache - Fast, stable PHP opcode cacher
php5-xdebug - Xdebug Module for PHP 5
php5-xmlrpc - XML-RPC module for php5
php5-xsl - XSL module for php5
phpunit - Unit testing suite for PHP5
root@satoraspi:~#

 はあ、いっぱい出たけど、要するにPHP5があるんだよな。うん、うん。

root@satoraspi:~# apt-get -s install php5
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
以下の特別パッケージがインストールされます:
  apache2-mpm-prefork libapache2-mod-php5 libonig2 libqdbm14 lsof php5-cli php5-common
提案パッケージ:
  php-pear
以下のパッケージは「削除」されます:
  apache2-mpm-worker
以下のパッケージが新たにインストールされます:
  apache2-mpm-prefork libapache2-mod-php5 libonig2 libqdbm14 lsof php5 php5-cli
  php5-common
アップグレード: 0 個、新規インストール: 8 個、削除: 1 個、保留: 0 個。
Remv apache2-mpm-worker [2.2.22-13+deb7u6] [apache2:armhf ]
Inst apache2-mpm-prefork (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Conf apache2-mpm-prefork (2.2.22-13+deb7u6 Raspbian:7.0/oldstable [armhf])
Inst lsof (4.86+dfsg-1 Raspbian:7.0/oldstable [armhf])
Inst php5-common (5.4.44-0+deb7u1 Raspbian:7.0/oldstable [armhf])
Inst libonig2 (5.9.1-1 Raspbian:7.0/oldstable [armhf])
Inst libqdbm14 (1.8.78-2 Raspbian:7.0/oldstable [armhf])
Inst libapache2-mod-php5 (5.4.44-0+deb7u1 Raspbian:7.0/oldstable [armhf])
Inst php5 (5.4.44-0+deb7u1 Raspbian:7.0/oldstable [all])
Inst php5-cli (5.4.44-0+deb7u1 Raspbian:7.0/oldstable [armhf])
Conf lsof (4.86+dfsg-1 Raspbian:7.0/oldstable [armhf])
Conf php5-common (5.4.44-0+deb7u1 Raspbian:7.0/oldstable [armhf])
Conf libonig2 (5.9.1-1 Raspbian:7.0/oldstable [armhf])
Conf libqdbm14 (1.8.78-2 Raspbian:7.0/oldstable [armhf])
Conf libapache2-mod-php5 (5.4.44-0+deb7u1 Raspbian:7.0/oldstable [armhf])
Conf php5 (5.4.44-0+deb7u1 Raspbian:7.0/oldstable [all])
Conf php5-cli (5.4.44-0+deb7u1 Raspbian:7.0/oldstable [armhf])
root@satoraspi:~#

 入れても大丈夫みたいだ。

root@satoraspi:~# apt-get  install php5
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
以下の特別パッケージがインストールされます:
  apache2-mpm-prefork libapache2-mod-php5 libonig2 libqdbm14 lsof php5-cli php5-common
提案パッケージ:
  php-pear
以下のパッケージは「削除」されます:
  apache2-mpm-worker
以下のパッケージが新たにインストールされます:
  apache2-mpm-prefork libapache2-mod-php5 libonig2 libqdbm14 lsof php5 php5-cli
  php5-common
アップグレード: 0 個、新規インストール: 8 個、削除: 1 個、保留: 0 個。
6,142 kB のアーカイブを取得する必要があります。
この操作後に追加で 17.3 MB のディスク容量が消費されます。
続行しますか [Y/n]? y
取得:1 http://mirrordirector.raspbian.org/raspbian/ wheezy/main apache2-mpm-prefork armhf 2.2.22-13+deb7u6 [2,342 B]
取得:2 http://mirrordirector.raspbian.org/raspbian/ wheezy/main lsof armhf 4.86+dfsg-1 [321 kB]
取得:3 http://mirrordirector.raspbian.org/raspbian/ wheezy/main php5-common armhf 5.4.44-0+deb7u1 [621 kB]
取得:4 http://mirrordirector.raspbian.org/raspbian/ wheezy/main libonig2 armhf 5.9.1-1 [130 kB]
取得:5 http://mirrordirector.raspbian.org/raspbian/ wheezy/main libqdbm14 armhf 1.8.78-2 [119 kB]
取得:6 http://mirrordirector.raspbian.org/raspbian/ wheezy/main libapache2-mod-php5 armhf 5.4.44-0+deb7u1 [2,479 kB]
取得:7 http://mirrordirector.raspbian.org/raspbian/ wheezy/main php5 all 5.4.44-0+deb7u1 [1,024 B]
取得:8 http://mirrordirector.raspbian.org/raspbian/ wheezy/main php5-cli armhf 5.4.44-0+deb7u1 [2,469 kB]
6,142 kB を 6秒 で取得しました (1,016 kB/s)
dpkg: apache2-mpm-worker: 依存関係に問題があります。しかし要求に従い削除しています:
 apache2 は以下に依存 (depends) します: apache2-mpm-worker (= 2.2.22-13+deb7u6) | apache2-mpm-prefork (= 2.2.22-13+deb7u6) | apache2-mpm-event (= 2.2.22-13+deb7u6) | apache2-mpm-itk (= 2.2.22-13+deb7u6) ...しかし:
  パッケージ apache2-mpm-worker は削除されようとしています。
  パッケージ apache2-mpm-prefork はまだインストールされていません。
  パッケージ apache2-mpm-event はまだインストールされていません。
  パッケージ apache2-mpm-itk はまだインストールされていません。

(データベースを読み込んでいます ... 現在 79208 個のファイルとディレクトリがインストールされています。)
apache2-mpm-worker を削除しています ...
[....] Stopping web server: apache2apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName
[ ok waiting ..
以前に未選択のパッケージ apache2-mpm-prefork を選択しています。
(データベースを読み込んでいます ... 現在 79203 個のファイルとディレクトリがインストールされています。)
(.../apache2-mpm-prefork_2.2.22-13+deb7u6_armhf.deb から) apache2-mpm-prefork を展開してい ます...
apache2-mpm-prefork (2.2.22-13+deb7u6) を設定しています ...
[....] Starting web server: apache2apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName
. ok
以前に未選択のパッケージ lsof を選択しています。
(データベースを読み込んでいます ... 現在 79208 個のファイルとディレクトリがインストールされています。)
(.../lsof_4.86+dfsg-1_armhf.deb から) lsof を展開しています...
以前に未選択のパッケージ php5-common を選択しています。
(.../php5-common_5.4.44-0+deb7u1_armhf.deb から) php5-common を展開しています...
以前に未選択のパッケージ libonig2 を選択しています。
(.../libonig2_5.9.1-1_armhf.deb から) libonig2 を展開しています...
以前に未選択のパッケージ libqdbm14 を選択しています。
(.../libqdbm14_1.8.78-2_armhf.deb から) libqdbm14 を展開しています...
以前に未選択のパッケージ libapache2-mod-php5 を選択しています。
(.../libapache2-mod-php5_5.4.44-0+deb7u1_armhf.deb から) libapache2-mod-php5 を展開しています...
以前に未選択のパッケージ php5 を選択しています。
(.../php5_5.4.44-0+deb7u1_all.deb から) php5 を展開しています...
以前に未選択のパッケージ php5-cli を選択しています。
(.../php5-cli_5.4.44-0+deb7u1_armhf.deb から) php5-cli を展開しています...
man-db のトリガを処理しています ...
lsof (4.86+dfsg-1) を設定しています ...
php5-common (5.4.44-0+deb7u1) を設定しています ...

Creating config file /etc/php5/mods-available/pdo.ini with new version
libonig2 (5.9.1-1) を設定しています ...
libqdbm14 (1.8.78-2) を設定しています ...
libapache2-mod-php5 (5.4.44-0+deb7u1) を設定しています ...

Creating config file /etc/php5/apache2/php.ini with new version
[....] Restarting web server: apache2apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName
 ... waiting apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName
. ok
php5 (5.4.44-0+deb7u1) を設定しています ...
php5-cli (5.4.44-0+deb7u1) を設定しています ...

Creating config file /etc/php5/cli/php.ini with new version
update-alternatives: /usr/bin/php (php) を提供するために 自動モード で /usr/bin/php5 を使います
root@satoraspi:~#

……と、いうわけで、サクッと入る。

PHPの作動を確かめる

 それでは早速……。

root@satoraspi:/var/www# ログアウト
toshio@satoraspi:~$ cd /var/www
toshio@satoraspi:/var/www$ vi infotest.php

 で、中身はこじんまりとこう書いて……

<html>
<head></head>
<body>
<?phpinfo();?>
</body>
</html>

 早速ブラウザで見ると……

raspi-phpinfo

うん。動いちょる動いちょる。

さっそくPHPでLチカ

 さて、次に、phpからGPIOだな。マンネリだけどやっぱり「Lチカ」か。

「web2LED.php」

<html>
 <head>
  <meta name="Editor" content="vim">
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
  <link rel="icon" href="favicon.ico" type="image/x-icon" />
  <link rel="Shortcut Icon" type="image/x-icon" href="favicon.ico" />
  <title>PHPでLチカ</title>
  <meta http-equiv="Keyword" content="佐藤俊夫, 佐藤, 俊夫, SATOTOSHIO, SatoToshio, sato, toshio, Raspberry Pi, Raspberry Pi 2 Model B">
 </head>
 <body bgcolor="#888888">
<body>
  <center>
  <h1>PHPでLチカ</h1>
  <hr>
  <form method="POST">
  <input type="submit" name="on"  value="LED on"><br>
  <input type="submit" name="off" value="LED off">
  </form>
  </center>
<?
ini_set( 'display_errors', 1 );
file_put_contents('/sys/class/gpio/export', 17);
file_put_contents('/sys/class/gpio/gpio17/direction', 'out');
if(isset($_POST['on'])){
  file_put_contents('/sys/class/gpio/gpio17/value', 1);
  print("LED ON.");
}elseif(isset($_POST['off'])){
  file_put_contents('/sys/class/gpio/gpio17/value', 0);
  print("LED OFF.");
}
file_put_contents('/sys/class/gpio/unexport', 17);
?>
</body>
</html>

 ブレッドボードにLEDと330Ωの抵抗を直列につけて、17番ピンとアースにつなぐ。

IMG_3313

 で、こういうフォームになるのだが……

web2LED_permission_deny

 いや、そりゃまあ、動くわけないわな、GPIOはrootでなきゃ読み書きできんのだから。

GPIOのパーミションの調整

 えーっと、どうやったらいいかな、ということで、

  • 「sudoする」
  • 「GPIOのアクセス権をヌルくする」
  • 「PHPの実行ユーザを強くする」
  • 「何かをSUID」

……などと、いろいろあるが、どうも、どれもいまいち、ピンと来ないな……。

 まあ、GPIOのアクセスをヌルくする、これかなあ……。

toshio@satoraspi:~$ cd /sys/class
toshio@satoraspi:/sys/class$ su
パスワード:
root@satoraspi:/sys/class# chmod -R 777 gpio
root@satoraspi:/sys/class# ls -Flad gpio
drwxrwxrwx 2 root gpio 0  8月 30 13:20 gpio/
root@satoraspi:/sys/class#

 なんっか、もう、力いっぱい777丸出しですけどね(笑)。多分、700とか760でも大丈夫じゃないかなあ。試してないけど。

 で、まあ、これで、フォームをクリックするとLEDが点いたり消えたりする。

Raspberry Pi + Apache + PHP + SSRでAC100Vの家電製品を制御

 これをそのまま、いつぞやArduinoでやったSSR(ソリッド・ステート・リレー)につなぐと、家電製品のオン・オフなどができるわけだ。

SSRモジュール
IMG_3047

 では、やってみよう。

Raspberry Pi 2で扇風機のオン・オフ

 真ん中の黒い箱がSSRだ。Arduinoは5V、Raspberry Pi 2は3.3Vで電圧が違うが、このSSRは3Vから8Vまでの入力を受け付けるので、大丈夫なのである。

ネット便器

投稿日:

 時代はIoTである。モノをネットに接続することは、もはや正義を通り越して神の啓示であるとすら言えるほどだ(笑)。

 やろうやろうと思ってやっていない一事はこのことだ。すなわち、「Arduino」を使用して、直接モノにツイートさせること、これである。モノがツイートするのはきっと面白いに違いない。

 「Twitter Arduino」あたりで検索すると、ライブラリが出てくる。今回はこれ(Tweet Library 1.3)をありがたく使わせていただく。

 さて、そうと決まれば何をネットに接続するか、である。ここはやはり、自宅の便器をネットに接続するという、これを一度やってみるべきであろう。

 やはり、IoT時代であるから、便器もネットにつないでやらなければ面白くないだろう。便器だって平等に扱ってやらねば、他の物品との差別感を覚えてひがむようになり、性格が曲がってしまうかもしれない。このように性格の歪んだ便器は、人間様が用を足すときに局部に噛みついてくるようなことが万に一つはないとも限らない。便器にも時々はインターネットと会話をさせてやるべきだ。

 とりあえず、便所のフタの開閉を検知するため、「チルトスイッチ」を使用する。

チルトスイッチ
IMG_3218

 これは、傾けるとスイッチが入るというもので、Arduinoを買ったときに一緒に買った「Seeedstudio SIDEKICK BASIC KIT」というセットに入っていたものだ。

 回路は簡単である。下図のように、行儀よく10kΩほどプルダウンしておけば誤動作は少ない。

チルトスイッチ回路図

 スケッチのほうは、ライブラリの導入に多少手間取った。使用させていただいた「Tweet Library」は、「ETHERNET SHIELD」のほうに対応しており、私が持っている「ETHERNET SHIELD 2」にはそのままでは対応していない。

 基本的にヘッダファイルのインクルードを「#include <Ethernet.h>」から「#include <Ethernet2.h>」に書き換えるだけでいいのでは、と思ったのだが、どうもうまくいかない。さっぱりお手上げだったのだが、いろいろといじくりまわしているうち、エラーメッセージをよく見てみると、「クラスの2重定義」という意味のエラーが出ていることがわかった。なぜか、「libraries\Ethernet2\srcの下にあるやつと重なっている」みたいなメッセージが出ている。ハテ、とライブラリのあるディレクトリを見てみると、「libraries\Ethernet2\src」の下に、なぜか「Twitter.h」と「Twitter.cpp」がある。

 なんだかわかんないけどいいや、消しちゃえ!と、それをぞんざい適当に消したら、うまくコンパイルできるようになった。しかし、そのことで5~6時間ほどハマッてしまった。

 ETHERNET SHIELD 2とプロトタイプシールドを、先日買った「継ぎ足しピンヘッダ」を介して積み重ね、次のようにしていつもの100円ショップのアクリル枠にねじ止めする。

IMG_3213

 手前に緑色のチルトスイッチが取り付けられていることがわかるだろう。

 Twitterに専用アカウントをとり、Tweet Libraryの説明にしたがってトークンを取得する。専用アカウントは、その名も「佐藤家の物」である(笑)。

 それから、スケッチを次のように書く。

//
//  つぶやき便所 tweetToilet.ino
//    27.08.08(土) 1000~
//    佐藤俊夫
//    チルトスイッチで便所のふたの動きを検出し、呟かせる。
//
#include <SPI.h>
#include <Ethernet2.h>
#include <Twitter.h>
#include <stdio.h>
//
byte MAC[] = { 0x90, 0xA2, 0xDA, 0x0F, 0xF6, 0x74 };
IPAddress IP(192, 168, 1, 129);
Twitter TWITTER("hogehogehoge-hagehagehagehage......");  //  トークン
const int TILTSW = 9;
//
void setup()
{
  pinMode(TILTSW, INPUT);
  delay(1000);
  Ethernet.begin(MAC, IP);
  delay(1000);
}

void loop()
{
  static int tiltSwStatus = LOW, prevStatus = LOW;
  int i = 0;
  tiltSwStatus = tiltSw();
  if(tiltSwStatus != prevStatus){
    prevStatus = tiltSwStatus;
    tweetMsg(tiltSwStatus);
    delay(1000);
  }else{
    ;
  }
}
//
int tiltSw(){
  //  チルトスイッチの読み取りを安定させるため、100回連続して同じ値が返るまで読む。
  int i = 0, prevStatus = LOW, nowStatus = LOW;
  prevStatus = digitalRead(TILTSW);
  do{
    nowStatus = digitalRead(TILTSW);
    if(nowStatus == prevStatus){
      i ++;
    }else{
      i = 0;
    }
    prevStatus = nowStatus;
  }while(i < 100);
  return(nowStatus);
}
//
void tweetMsg(int tiltStatus){
  const char
    openMsg[] = "%e3%82%84%e3%81%82%e3%80%82%e4%bf%ba%e3%81%af%e4%bd%90%e8%97%a4%e5%ae%85%e3%81%ae%e4%be%bf%e5%99%a8%e3%81%a0%e3%80%82%e4%bb%8a%e3%83%95%e3%82%bf%e3%81%8c%e9%96%8b%e3%81%84%e3%81%a6%e3%81%84%e3%82%8b%e3%80%82",
    //  'やあ。俺は佐藤宅の便器だ。今フタが開いている。'    
    closeMsg[] = "%e4%bf%ba%e3%81%af%e4%bd%90%e8%97%a4%e5%ae%85%e3%81%ae%e4%be%bf%e5%99%a8%e3%81%a0%e3%80%82%e4%bb%8a%e3%83%95%e3%82%bf%e3%81%8c%e9%96%89%e3%81%be%e3%81%a3%e3%81%9f%e3%80%82";
    //  '俺は佐藤宅の便器だ。今フタが閉まった。'
  char tweetStr[256];
  if(tiltStatus == HIGH){
    sprintf(tweetStr, "%s  \r\n %ld", openMsg, millis());
    //  Twitterは同じ文字列を繰り返し書き続けられないので、起動時間を付けて書き、重複を防ぐ。
  }else{
    sprintf(tweetStr, "%s  \r\n %ld", closeMsg,  millis());
  }
  TWITTER.post(tweetStr);
  TWITTER.wait();
}

 うまく動くようになったら、ホット・グルーでプロトタイプを便器のフタに取り付ける。

IMG_3216IMG_3215

 便所にLAN工事を施しておしまいだ。

IMG_3214
 

 上記動画のように便器のふたを開け閉めすると、やおら便器が次のように呟きだすのである。



呻吟

投稿日:

 「ETHERNET SHIELD 2」でTwitterに投稿しようとすると、よく知られているライブラリがそのままでは使えないということがわかり、呻吟する。

 しかしまあ、所詮ウェブであるから、TCP/IPソケットさえあれば、低いレイヤから積み上げることでツイートはできるだろう。

 ……それをやる気があるかどうかの問題だな。うーん。どうしよう。

空の写真リベンジ

投稿日:

 先週試したAdafruit製の「小型TTLシリアルjpegカメラ」での間欠撮影先週はどうしたわけか同じ画像ばかり撮れてしまい、失敗である。

 (チナミに、このカメラのメーカーの「Adafruit」という会社、有名なマッシモ・バンジのTEDの中で紹介されていたことに気付いた。)

https://youtube.com/watch?v=UoBUXOOdLXY%3Ft%3D5m50s

 気を取り直して、スケッチを直す。撮影時のカメラのステータスを確認し、撮れていなければ何回でも撮り続ける。

「while(撮れてない)撮る;」

……というわけだ。まあ、万が一ハードウェアエラーなどがあるとループが回り続けるので良くないが、ループが回り続けようが結局のところはモノとしては電源を入れ直す他にどうしようもないので、こんなものだろう。

 それと、撮影が終わったらカメラをそのつどリセットすることにした。また、既に存在するファイル名は避けるようにした。こういう時、Arduinoには書式文字列付きの「sprintf」がないので、少し不便だなと思う。

 夜明けから日没までの一日の空の雲を撮りたいので、天気予報を見て雨が降らぬ確信を持ってから、昨夜寝る前にベランダにカメラをセットしておいた。朝早く起きるより楽だからだ。夜のうちに1000枚くらい写真が撮れてしまうが、SDカードには余裕があるので大丈夫である。

 昼間は用事があるのでカメラの面倒は見れないが、放置しておけば淡々と写真は撮れていく。

IMG_3143

 で、撮れた写真をWindows Movie Makerに流し込むと、微速撮影動画の一丁上がりだ。

 スケッチは次のとおりである。

//
//  camera2Web.ino
//    27.07.20(月) 0850~
//    佐藤俊夫
//    Adafruit製小型TTLシリアルJPEGカメラ+ETHERNET SHIELD 2で
//    間欠撮影をし、Webでダウンロードできるようにする。
//
#include <Adafruit_VC0706.h>
#include <SPI.h>
#include <SD.h>
#include <SoftwareSerial.h>
#include <Ethernet2.h>

#define CHIPSELECT 4

SoftwareSerial CAMCONNECTION(2, 3);
Adafruit_VC0706 CAM = Adafruit_VC0706(&CAMCONNECTION);
const unsigned long int INTERVAL = 30L * 1000L;
byte MAC[] = {  0x90, 0xA2, 0xDA, 0x0F, 0xF6, 0x74 };
IPAddress IP(192, 168, 1, 129);
EthernetServer SERVER(80);
EthernetClient CLIENT;

void setup() {
  pinMode(10, OUTPUT);
  if(!SD.begin(CHIPSELECT)) return;
  if(!CAM.begin()) return;
  CAM.setImageSize(VC0706_320x240);
  Ethernet.begin(MAC, IP);
  SERVER.begin();
  delay(1000);
}

void loop() {
  static unsigned long int prevtime = 0;
  char c;
  String rstr = "";
  //  INTERVALおきに写真を撮る
  if(millis() >= prevtime + INTERVAL){
    prevtime = millis();
    takePicture();
  }
  //  Webサーバ
  CLIENT = SERVER.available();
  if(CLIENT) {
    while(CLIENT.connected()) {
      if(CLIENT.available()) {
        c = CLIENT.read();
        rstr += c;
        if(rstr.endsWith("\r\n")){
          break;
        }
      }
    }
    if(rstr.indexOf("IMG") >= 0){
      String filename = "DCIM/";
      char cfilename[17];
      filename.concat(rstr.substring(rstr.indexOf("IMG"), rstr.indexOf("JPG") + 3));
      filename.toCharArray(cfilename, 17);
      CLIENT.println("HTTP/1.1 200 OK");
      CLIENT.println("Content-Type: image/jpg");
      CLIENT.println("Connection: close");
      CLIENT.println();
      File img = SD.open(cfilename);
      while(img.available()){
        CLIENT.write(img.read());
      }
      img.close();
    }else{
      sendform();
    }
    rstr = "";
    delay(1);
    // close the connection:
    CLIENT.stop();
  }
  delay(20);
}

void sendform(){
  //  フォームを送る。
  CLIENT.println("HTTP/1.1 200 OK");
  CLIENT.println("Content-Type: text/html");
  CLIENT.println("Connection: close");
  CLIENT.println();
  CLIENT.println("<!DOCTYPE HTML>");
  CLIENT.println("<html><head></head><body><center>");
  File dcim = SD.open("/DCIM");
  while(true) {
    File imgfile =  dcim.openNextFile();
    if(!imgfile){
      dcim.rewindDirectory();
      break;
    }
    CLIENT.write("<a href=\"");
    CLIENT.write(imgfile.name());
    CLIENT.write("\">");    
    CLIENT.write(imgfile.name());
    CLIENT.println("</a><br>");
    imgfile.close();    
  }
  dcim.close();
  CLIENT.println("</center></body></html>");
}

void takePicture(){
  static unsigned int pnum = 0;
  char filename[] = "DCIM/img0000.jpg";
  while(!CAM.takePicture());
  do{
    filename[8]  = '0' + pnum / 1000;
    filename[9]  = '0' + (pnum / 100) % 10;
    filename[10] = '0' + (pnum /  10) % 10;
    filename[11] = '0' + pnum % 10;
    pnum ++;
  }while(SD.exists(filename));
  if(pnum > 9999) pnum = 0;
  File imgFile = SD.open(filename, FILE_WRITE);
  uint16_t jpglen = CAM.frameLength();
  pinMode(8, OUTPUT);
  while (jpglen > 0) {
    uint8_t *buffer;
    uint8_t bytesToRead = min(32, jpglen);
    buffer = CAM.readPicture(bytesToRead);
    imgFile.write(buffer, bytesToRead);
    jpglen -= bytesToRead;
  }
  while(!CAM.reset());
  imgFile.close();
}




Webでサービスできるカメラ

投稿日:

 一昨日遊んだカメラ。今日はこれにETHERNET SHIELD 2を取り付けて遊んでみる。

  •  間欠的に写真を撮り続ける。
  •  SDに保存する。
  •  ウェブアクセスがあると、一覧でサービスする。

 まあ、これくらいで。サムネイル画像などハデに出したい気もするが、Arduinoはメモリも小さく、そんなに気の利いたウェブサービスはできない。

 プロトタイピングシールドのブレッドボードに組み付ける。

IMG_3136

 まず、スケッチはこんなところだろう。

//
//  camera2Web.ino
//    27.07.20(月) 0850~
//    佐藤俊夫
//    Adafruit製小型TTLシリアルJPEGカメラ+ETHERNET SHIELD 2で
//    間欠撮影をし、Webでダウンロードできるようにする。
//
#include <Adafruit_VC0706.h>
#include <SPI.h>
#include <SD.h>
#include <SoftwareSerial.h>
#include <Ethernet2.h>

#define CHIPSELECT 4

SoftwareSerial CAMCONNECTION(2, 3);
Adafruit_VC0706 CAM = Adafruit_VC0706(&CAMCONNECTION);
const unsigned long int INTERVAL = 30L * 1000L;
byte MAC[] = {  0x90, 0xA2, 0xDA, 0x0F, 0xF6, 0x74 };
IPAddress IP(192, 168, 1, 129);
EthernetServer SERVER(80);
EthernetClient CLIENT;

void setup() {
  pinMode(10, OUTPUT);
  if(!SD.begin(CHIPSELECT)) return;
  if(!CAM.begin()) return;
  CAM.setImageSize(VC0706_320x240);
  Ethernet.begin(MAC, IP);
  SERVER.begin();
  delay(1000);
}

void loop() {
  static unsigned long int prevtime = 0;
  char c;
  String rstr = "";
  //  INTERVALおきに写真を撮る
  if(millis() >= prevtime + INTERVAL){
    prevtime = millis();
    takePicture();
  }
  //  Webサーバ
  CLIENT = SERVER.available();
  if(CLIENT) {
    while(CLIENT.connected()) {
      if(CLIENT.available()) {
        c = CLIENT.read();
        rstr += c;
        if(rstr.endsWith("\r\n")){
          break;
        }
      }
    }
    if(rstr.indexOf("IMG") >= 0){
      String filename = "DCIM/";
      char cfilename[17];
      filename.concat(rstr.substring(rstr.indexOf("IMG"), rstr.indexOf("JPG") + 3));
      filename.toCharArray(cfilename, 17);
      CLIENT.println("HTTP/1.1 200 OK");
      CLIENT.println("Content-Type: image/jpg");
      CLIENT.println("Connection: close");
      CLIENT.println();
      File img = SD.open(cfilename);
      while(img.available()){
        CLIENT.write(img.read());
      }
      img.close();
    }else{
      sendform();
    }
    rstr = "";
    delay(1);
    // close the connection:
    CLIENT.stop();
  }
  delay(20);
}

void sendform(){
  //  フォームを送る。
  CLIENT.println("HTTP/1.1 200 OK");
  CLIENT.println("Content-Type: text/html");
  CLIENT.println("Connection: close");
  CLIENT.println();
  CLIENT.println("<!DOCTYPE HTML>");
  CLIENT.println("<html><head></head><body><center>");
  File dcim = SD.open("/DCIM");
  while(true) {
    File imgfile =  dcim.openNextFile();
    if(!imgfile){
      dcim.rewindDirectory();
      break;
    }
    CLIENT.write("<a href=\"");
    CLIENT.write(imgfile.name());
    CLIENT.write("\">");    
    CLIENT.write(imgfile.name());
    CLIENT.println("</a><br>");
    imgfile.close();    
  }
  dcim.close();
  CLIENT.println("</center></body></html>");
}

void takePicture(){
  static unsigned int pnum = 0;
  char filename[] = "DCIM/img0000.jpg";
  CAM.takePicture();
  filename[8]  = '0' + pnum / 1000;
  filename[9]  = '0' + (pnum / 100) % 10;
  filename[10] = '0' + (pnum /  10) % 10;
  filename[11] = '0' + pnum % 10;
  pnum ++;
  if(pnum > 9999) pnum = 0;
  if(SD.exists(filename)) return;
  File imgFile = SD.open(filename, FILE_WRITE);
  uint16_t jpglen = CAM.frameLength();
  pinMode(8, OUTPUT);
  while (jpglen > 0) {
    uint8_t *buffer;
    uint8_t bytesToRead = min(32, jpglen);
    buffer = CAM.readPicture(bytesToRead);
    imgFile.write(buffer, bytesToRead);
    jpglen -= bytesToRead;
  }
  imgFile.close();
}

  で、電源をつないで放置しておくと、写真が撮れるわけだ。既存のファイル名を避けるような芸コマなことはしてないけど、これはこれでけっこう面白い。

 いつもの100円ショップのアクリル枠にねじ止めする。このカメラは上下が逆に映るので、こうすると丁度カメラが逆さまになって具合が良い。

IMG_3138

 それで、以前にWIZnetで紹介されて愉快だったアレのリベンジをやってみよう。携帯用のカメラホルダーに取り付け、ベランダに出して放置。

IMG_3139

いよいよ多機能リモコン

投稿日:

 LEDを強力に光らせることが出来たので、いよいよWebサーバつき多機能リモコンを作成する。卓上などに置いておき、ネットワークにつないで、スマホなどから複数の電化製品を操作できるというものだ。

 ここでは、いくつかのテクニックを使った。

 一つは、フォームが大きくなってしまい、ハードコーディングするとメモリが足りなくなる。そこで、SDカード内にHTMLを置き、これを読み出すようにした。

 同様に、リモコンから読み取った数値データが大きくなって、普通にハードコーディングしたのではメモリが不足する。そこで、「PROGMEM」というキーワードを使って、フラッシュメモリ内にデータを置き、これを読み出すようにした。

IMG_3134 ETHERNET SHIELD 2を遺憾なく使う。SDカードを取り付けられるから、そこにHTMLを書き込んでおけばよい。FETをつけたブレッドボードと一緒に、買っておいたSparkfunの基盤に固定する。

 SDカード内には、次のようなHTMLを置き、ファイル名を「irform.htm」とする。

<html>
 <head>
  <meta name="Editor" content="Notepad.exe">
  <meta http-equiv="Content-Type" content="text/html;charset=Shift_JIS">
  <title>Webリモコン</title>
  <basefont size=4>
 </head>

 <body bgcolor="#ddffdd">
  <center>
   <h1><b>Webリモコン</b></h1>
     <table>
      <tr><td> 作成者   </td>
       <td align="right">佐藤俊夫</td></tr>
      <tr><td> 作成日時 </td>
       <td align="right">27.07.19 (日) 1835</td></tr>
     </table>
  </center>
  <hr>
  <center>
    <form method="get" name="irremote">
    <table border=1>
      <tr><th>機器</th><th>ボタン</th></tr>
      <tr>
        <td rowspan=4>扇風機</td>
        <td><input submit type="submit" value="入/風量" name="fan_on"></td>
      </tr>
        <tr><td><input submit type="submit" value="タイマー" name="fan_timer"></td></tr>
        <tr><td><input submit type="submit" value="首振" name="fan_swing"></td></tr>
        <tr><td><input submit type="submit" value="切" name="fan_off"></td></tr>
      </tr>
      <tr>
        <td rowspan=5>テレビ</td>
        <td><input submit type="submit" value="入/切" name="tv_on_off"></td>
      </tr>
        <tr><td><input submit type="submit" value="音量大" name="tv_volup"></td></tr>
        <tr><td><input submit type="submit" value="音量小" name="tv_voldown"></td></tr>
        <tr><td><input submit type="submit" value="チャンネル>" name="tv_chup"></td></tr>
        <tr><td><input submit type="submit" value="チャンネル<" name="tv_chdown"></td></tr>
    </table>
    </form>
  </center>
 </body>
</html>

irform 上のHTMLの見た目はこんな感じだ。

 スケッチは次のようになる。

//
//  Web2IRremote.ino
//    リモコンをウェブで操作する。
//    27.07.25(日) 1930
//    佐藤俊夫
//
#include <SPI.h>
#include <Ethernet2.h>
#include <SD.h>
#include <IRremote.h>
#include <avr/pgmspace.h>
//
byte mac[] = {  0x90, 0xA2, 0xDA, 0x0F, 0xF6, 0x74 };
IPAddress ip(192, 168, 1, 129);
EthernetServer SERVER(80);
EthernetClient CLIENT;
IRsend irsend;
PROGMEM
  const unsigned int
    fan_on[]     = {4500,2150, 600,500, 650,500, 600,500, 600,500, 600,1650, 600,1600, 650,1550, 650,500, 600,1600, 650,1600, 600,1600, 650,450, 600,1600, 650,500, 600,500, 650,500, 600,1600, 600,500, 650,500, 600,500, 600,500, 600,500, 600,1650, 600,500, 600,500, 600,500, 650,1600, 600,1600, 600,1650, 600,1600, 600,500, 650,500, 600,0},
    fan_timer[]  = {4500,2150, 550,550, 550,600, 550,550, 600,500, 550,1700, 500,1700, 550,1650, 550,600, 550,1650, 550,1700, 500,1700, 550,600, 500,1700, 550,550, 550,550, 550,600, 500,1700, 550,600, 500,600, 500,600, 500,600, 550,550, 550,1700, 550,600, 450,600, 550,550, 550,600, 500,1700, 550,1650, 550,1700, 550,1650, 550,600, 500,0},
    fan_swing[]  = {4450,2200, 600,550, 550,500, 600,550, 550,550, 550,1700, 550,1650, 550,1700, 550,550, 550,1650, 550,1700, 550,1650, 550,550, 600,1650, 550,550, 550,550, 550,550, 600,1650, 550,550, 550,550, 600,550, 550,550, 550,550, 600,1600, 600,550, 550,550, 550,1650, 600,1550, 650,1700, 550,1650, 550,550, 550,550, 600,550, 550,0},
    fan_off[]    = {4450,2250, 550,550, 600,550, 550,500, 600,550, 550,1650, 600,1650, 550,1650, 600,550, 550,1650, 550,1650, 600,1650, 550,550, 550,1700, 550,550, 550,550, 550,550, 550,1700, 550,550, 550,550, 550,550, 600,550, 550,550, 550,1650, 550,550, 600,550, 550,1650, 600,1650, 550,550, 550,1700, 550,500, 600,550, 550,1650, 600,0},
    tv_on_off[]  = {3400,1750, 400,500, 350,1350, 400,500, 350,500, 400,500, 350,500, 400,500, 350,450, 400,500, 400,500, 350,500, 400,500, 350,500, 350,1350, 400,500, 400,500, 350,450, 400,500, 400,500, 350,450, 400,500, 400,500, 350,500, 400,1300, 400,500, 400,450, 400,500, 400,500, 350,500, 350,500, 400,450, 400,450, 400,1350, 400,500, 400,1300, 400,1350, 400,1350, 400,1350, 400,450, 400,500, 400,1300, 400,500, 400,1300, 400,1350, 400,1350, 400,1350, 400,500, 350,1350, 400,0},
    tv_volup[]   = {3400,1750, 400,500, 400,1300, 400,500, 400,500, 350,500, 450,400, 400,500, 350,500, 400,500, 350,500, 350,500, 400,500, 350,500, 400,1300, 400,500, 400,450, 400,500, 350,500, 400,500, 350,500, 400,500, 350,500, 350,500, 400,1300, 450,450, 400,500, 350,500, 400,500, 350,500, 400,450, 400,500, 350,500, 400,500, 350,450, 400,500, 400,500, 350,500, 450,1250, 400,500, 400,500, 350,500, 400,450, 400,500, 350,500, 400,500, 350,1350, 400,450, 400,1350, 400,0},
    tv_voldown[] = {3400,1750, 400,500, 350,1350, 400,500, 400,450, 400,500, 350,500, 400,450, 400,500, 400,500, 350,500, 350,500, 400,500, 350,500, 350,1300, 450,500, 400,500, 350,450, 400,500, 450,450, 350,500, 400,450, 400,450, 400,500, 400,1300, 450,500, 350,500, 350,500, 400,500, 350,450, 400,500, 400,500, 350,500, 400,1300, 400,500, 400,450, 400,500, 400,500, 350,1350, 400,500, 350,500, 400,1300, 400,500, 400,500, 350,500, 350,500, 400,1300, 450,500, 350,1350, 400,0},
    tv_chup[]    = {3400,1750, 400,500, 400,1350, 450,400, 400,450, 400,500, 450,400, 400,450, 400,500, 450,450, 450,400, 450,450, 400,400, 400,500, 450,1250, 450,500, 350,450, 500,400, 450,400, 400,500, 350,500, 450,450, 350,500, 450,450, 450,1250, 400,500, 450,400, 400,500, 400,450, 450,400, 450,450, 350,500, 450,400, 450,450, 350,500, 450,1250, 400,500, 450,1250, 500,1250, 500,400, 450,450, 350,500, 450,450, 350,1350, 450,450, 400,1300, 450,1250, 500,450, 400,1250, 450,0},
    tv_chdown[]  = {3500,1650, 450,500, 400,1300, 400,500, 450,400, 350,500, 400,450, 500,400, 450,450, 350,450, 400,500, 400,500, 350,500, 400,450, 450,1300, 450,450, 350,450, 450,500, 350,500, 450,400, 450,400, 400,500, 350,500, 450,400, 400,1350, 450,400, 500,400, 450,450, 450,400, 400,500, 350,500, 350,500, 400,500, 350,1350, 450,450, 350,1350, 400,500, 350,1350, 500,1250, 450,450, 450,400, 450,1250, 500,400, 450,1300, 450,450, 400,1300, 450,1250, 450,500, 350,1350, 450,0};
////
void setup() 
{ 
  const int chipSelect = 4;
  Ethernet.begin(mac, ip);
  SERVER.begin();
  if (!SD.begin(chipSelect)) {
    return;
  }
} 
 
void loop() 
{ 
  //  Webサーバの動作
  char c;
  String rstr = "";
  CLIENT = SERVER.available();
  if (CLIENT) {
    while (CLIENT.connected()) {
      if (CLIENT.available()) {
        c = CLIENT.read();
        rstr += c;
        if(rstr.endsWith("\r\n")){
          break;
        }
      }
    }
    if(rstr.indexOf("fan_on=") >= 0){
      unsigned int buf[sizeof(fan_on) / sizeof(*fan_on)];
      for(int i = 0; i < sizeof(fan_on) / sizeof(*fan_on); i++){
        buf[i] = pgm_read_word(fan_on + i);
      }
      irsend.sendRaw(buf, sizeof(buf) / sizeof(buf[0]), 38);
    }
    else if(rstr.indexOf("fan_timer=") >= 0){
      unsigned int buf[sizeof(fan_timer) / sizeof(*fan_timer)];
      for(int i = 0; i < sizeof(fan_timer) / sizeof(*fan_timer); i++){
        buf[i] = pgm_read_word(fan_timer + i);
      }
      irsend.sendRaw(buf, sizeof(buf) / sizeof(buf[0]), 38);
    }
    else if(rstr.indexOf("fan_swing=") >= 0){
      unsigned int buf[sizeof(fan_swing) / sizeof(*fan_swing)];
      for(int i = 0; i < sizeof(fan_swing) / sizeof(*fan_swing); i++){
        buf[i] = pgm_read_word(fan_swing + i);
      }
      irsend.sendRaw(buf, sizeof(buf) / sizeof(buf[0]), 38);
    }
    else if(rstr.indexOf("fan_off=") >= 0){
     unsigned int buf[sizeof(fan_off) / sizeof(*fan_off)];
     for(int i = 0; i < sizeof(fan_off) / sizeof(*fan_off); i++){
        buf[i] = pgm_read_word(fan_off + i);
      }
      irsend.sendRaw(buf, sizeof(buf) / sizeof(buf[0]), 38);
    }
    else if(rstr.indexOf("tv_on_off=") >= 0){
     unsigned int buf[sizeof(tv_on_off) / sizeof(*tv_on_off)];
     for(int i = 0; i < sizeof(tv_on_off) / sizeof(*tv_on_off); i++){
        buf[i] = pgm_read_word(tv_on_off + i);
      }
      irsend.sendRaw(buf, sizeof(buf) / sizeof(buf[0]), 38);
    }
    else if(rstr.indexOf("tv_volup=") >= 0){
     unsigned int buf[sizeof(tv_volup) / sizeof(*tv_volup)];
     for(int i = 0; i < sizeof(tv_volup) / sizeof(*tv_volup); i++){
        buf[i] = pgm_read_word(tv_volup + i);
      }
      irsend.sendRaw(buf, sizeof(buf) / sizeof(buf[0]), 38);
    }
    else if(rstr.indexOf("tv_voldown=") >= 0){
     unsigned int buf[sizeof(tv_voldown) / sizeof(*tv_voldown)];
     for(int i = 0; i < sizeof(tv_voldown) / sizeof(*tv_voldown); i++){
        buf[i] = pgm_read_word(tv_voldown + i);
      }
      irsend.sendRaw(buf, sizeof(buf) / sizeof(buf[0]), 38);
    }
    else if(rstr.indexOf("tv_chup=") >= 0){
     unsigned int buf[sizeof(tv_chup) / sizeof(*tv_chup)];
     for(int i = 0; i < sizeof(tv_chup) / sizeof(*tv_chup); i++){
        buf[i] = pgm_read_word(tv_chup + i);
      }
      irsend.sendRaw(buf, sizeof(buf) / sizeof(buf[0]), 38);
    }
    else if(rstr.indexOf("tv_chdown=") >= 0){
     unsigned int buf[sizeof(tv_chdown) / sizeof(*tv_chdown)];
     for(int i = 0; i < sizeof(tv_chdown) / sizeof(*tv_chdown); i++){
        buf[i] = pgm_read_word(tv_chdown + i);
      }
      irsend.sendRaw(buf, sizeof(buf) / sizeof(buf[0]), 38);
    }
    rstr = "";
    sendform();
    delay(1);
    // close the connection:
    CLIENT.stop();
  }
  delay(20);
}
void sendform(){
  //  フォームを送る。
  CLIENT.println("HTTP/1.1 200 OK");
  CLIENT.println("Content-Type: text/html");
  CLIENT.println("Connection: close");
  CLIENT.println();
  CLIENT.println("<!DOCTYPE HTML>");
  File html = SD.open("irform.htm");
  if (html) {
    while (html.available()) {
      CLIENT.write(html.read());
    }
    html.close();
  } 
}

IMG_3135 勿論、ただのWebであるから、このようにスマホの他、タブレットなどからも操作可能である。また、ルータでポートフォワードすれば、外出先からの操作も可能である。

カメラで遊んでみる

投稿日:

 秋月電子のサイトを見ていると、小さなカメラがあり、Arduinoに付きそうな感じだ。

 3850円。Arduino自体が2800円かそこらなので、それに比べるとちょっと高いが、早速行って購入。

 だが、あまり情報は多くない。まず、メーカーのサイトを見ていくと、チュートリアルがあり、「とりあえずテストするには、電源をくれてやって、一番端のピンをテレビにつなぎゃあ絵が出る」みたいなザックリ感満載の解説が。それで、テレビにつなぐためのRCAジャックなども買う。

 チュートリアルはこれを読んでおけばだいたいいいようだ。

 ほどいてみるとこんな感じで、かなり小さい。

IMG_3112

 ピンのピッチが2mmで、ブレッドボードで扱いにくい。それで、普通の2.54mmのピンヘッダを出して、その根元をこんなふうにムリヤリ(笑)2mmピッチにせばめる。

IMG_3116

 こいつをカメラの基盤にえいやっ、とねじ込み、半田付けする。

IMG_3118

 なかなか小さいので、ルーペと老眼鏡を併用しつつ、ICなんか壊しちゃってもナンだから、20Wのぬるくて細い半田鏝でさっさとつける。

IMG_3117

 我ながらなかなかスピーディな仕事だなあ(笑)。

 で、メーカーのサイトには「5V」と書いてあるが、これは互換品の別の製品のためのチュートリアルのようで、買ってきたものの基盤をよく見ると「3.3V」と印刷されている。壊してはもったいないから、3.3Vで試す。3.3Vの電源代わりにArduinoの3.3Vピンを使う。

 基盤の印刷通り、3.3V、GND、それから右端のピンをRCAジャックのセンターに、RCAジャックのアースを同じくGNDに入れて、テレビの前に持っていく。

IMG_3119

 テレビにつなぐと、おお、確かに、値段なりのフザけた画質(笑)で、自分の顔が映る。

IMG_3121

 上下が逆だが、まあ、いいや。

 で、今度はArduinoで画像を撮影してみよう。

 チュートリアルにしたがってArduino用のライブラリをダウンロードし、これをArduinoのインストールフォルダの「libraries」に配置する。

 そうしておいてArduinoのIDEを起動すると、「ファイル」→「スケッチの例」の中に「Adafruit VC0706 Serial Camera Library」が現れるから、この中から「Snapshot」を選ぶ。これは静止画をjpegで撮影するスケッチのサンプルだ。

// This is a basic snapshot sketch using the VC0706 library.
// On start, the Arduino will find the camera and SD card and
// then snap a photo, saving it to the SD card.
// Public domain.

// If using an Arduino Mega (1280, 2560 or ADK) in conjunction
// with an SD card shield designed for conventional Arduinos
// (Uno, etc.), it's necessary to edit the library file:
//   libraries/SD/utility/Sd2Card.h
// Look for this line:
//   #define MEGA_SOFT_SPI 0
// change to:
//   #define MEGA_SOFT_SPI 1
// This is NOT required if using an SD card breakout interfaced
// directly to the SPI bus of the Mega (pins 50-53), or if using
// a non-Mega, Uno-style board.

#include <Adafruit_VC0706.h>
#include <SPI.h>
#include <SD.h>

// comment out this line if using Arduino V23 or earlier
#include <SoftwareSerial.h>         

// uncomment this line if using Arduino V23 or earlier
// #include <NewSoftSerial.h>       

// SD card chip select line varies among boards/shields:
// Adafruit SD shields and modules: pin 10
// Arduino Ethernet shield: pin 4
// Sparkfun SD shield: pin 8
// Arduino Mega w/hardware SPI: pin 53
// Teensy 2.0: pin 0
// Teensy++ 2.0: pin 20
#define chipSelect 10

// Pins for camera connection are configurable.
// With the Arduino Uno, etc., most pins can be used, except for
// those already in use for the SD card (10 through 13 plus
// chipSelect, if other than pin 10).
// With the Arduino Mega, the choices are a bit more involved:
// 1) You can still use SoftwareSerial and connect the camera to
//    a variety of pins...BUT the selection is limited.  The TX
//    pin from the camera (RX on the Arduino, and the first
//    argument to SoftwareSerial()) MUST be one of: 62, 63, 64,
//    65, 66, 67, 68, or 69.  If MEGA_SOFT_SPI is set (and using
//    a conventional Arduino SD shield), pins 50, 51, 52 and 53
//    are also available.  The RX pin from the camera (TX on
//    Arduino, second argument to SoftwareSerial()) can be any
//    pin, again excepting those used by the SD card.
// 2) You can use any of the additional three hardware UARTs on
//    the Mega board (labeled as RX1/TX1, RX2/TX2, RX3,TX3),
//    but must specifically use the two pins defined by that
//    UART; they are not configurable.  In this case, pass the
//    desired Serial object (rather than a SoftwareSerial
//    object) to the VC0706 constructor.

// Using SoftwareSerial (Arduino 1.0+) or NewSoftSerial (Arduino 0023 & prior):
#if ARDUINO >= 100
// On Uno: camera TX connected to pin 2, camera RX to pin 3:
SoftwareSerial cameraconnection = SoftwareSerial(2, 3);
// On Mega: camera TX connected to pin 69 (A15), camera RX to pin 3:
//SoftwareSerial cameraconnection = SoftwareSerial(69, 3);
#else
NewSoftSerial cameraconnection = NewSoftSerial(2, 3);
#endif

Adafruit_VC0706 cam = Adafruit_VC0706(&cameraconnection);

// Using hardware serial on Mega: camera TX conn. to RX1,
// camera RX to TX1, no SoftwareSerial object is required:
//Adafruit_VC0706 cam = Adafruit_VC0706(&Serial1);

void setup() {

  // When using hardware SPI, the SS pin MUST be set to an
  // output (even if not connected or used).  If left as a
  // floating input w/SPI on, this can cause lockuppage.
#if !defined(SOFTWARE_SPI)
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  if(chipSelect != 53) pinMode(53, OUTPUT); // SS on Mega
#else
  if(chipSelect != 10) pinMode(10, OUTPUT); // SS on Uno, etc.
#endif
#endif

  Serial.begin(9600);
  Serial.println("VC0706 Camera snapshot test");
  
  // see if the card is present and can be initialized:
  if (!SD.begin(chipSelect)) {
    Serial.println("Card failed, or not present");
    // don't do anything more:
    return;
  }  
  
  // Try to locate the camera
  if (cam.begin()) {
    Serial.println("Camera Found:");
  } else {
    Serial.println("No camera found?");
    return;
  }
  // Print out the camera version information (optional)
  char *reply = cam.getVersion();
  if (reply == 0) {
    Serial.print("Failed to get version");
  } else {
    Serial.println("-----------------");
    Serial.print(reply);
    Serial.println("-----------------");
  }

  // Set the picture size - you can choose one of 640x480, 320x240 or 160x120 
  // Remember that bigger pictures take longer to transmit!
  
  cam.setImageSize(VC0706_640x480);        // biggest
  //cam.setImageSize(VC0706_320x240);        // medium
  //cam.setImageSize(VC0706_160x120);          // small

  // You can read the size back from the camera (optional, but maybe useful?)
  uint8_t imgsize = cam.getImageSize();
  Serial.print("Image size: ");
  if (imgsize == VC0706_640x480) Serial.println("640x480");
  if (imgsize == VC0706_320x240) Serial.println("320x240");
  if (imgsize == VC0706_160x120) Serial.println("160x120");

  Serial.println("Snap in 3 secs...");
  delay(3000);

  if (! cam.takePicture()) 
    Serial.println("Failed to snap!");
  else 
    Serial.println("Picture taken!");
  
  // Create an image with the name IMAGExx.JPG
  char filename[13];
  strcpy(filename, "IMAGE00.JPG");
  for (int i = 0; i < 100; i++) {
    filename[5] = '0' + i/10;
    filename[6] = '0' + i%10;
    // create if does not exist, do not open existing, write, sync after write
    if (! SD.exists(filename)) {
      break;
    }
  }
  
  // Open the file for writing
  File imgFile = SD.open(filename, FILE_WRITE);

  // Get the size of the image (frame) taken  
  uint16_t jpglen = cam.frameLength();
  Serial.print("Storing ");
  Serial.print(jpglen, DEC);
  Serial.print(" byte image.");

  int32_t time = millis();
  pinMode(8, OUTPUT);
  // Read all the data up to # bytes!
  byte wCount = 0; // For counting # of writes
  while (jpglen > 0) {
    // read 32 bytes at a time;
    uint8_t *buffer;
    uint8_t bytesToRead = min(32, jpglen); // change 32 to 64 for a speedup but may not work with all setups!
    buffer = cam.readPicture(bytesToRead);
    imgFile.write(buffer, bytesToRead);
    if(++wCount >= 64) { // Every 2K, give a little feedback so it doesn't appear locked up
      Serial.print('.');
      wCount = 0;
    }
    //Serial.print("Read ");  Serial.print(bytesToRead, DEC); Serial.println(" bytes");
    jpglen -= bytesToRead;
  }
  imgFile.close();

  time = millis() - time;
  Serial.println("done!");
  Serial.print(time); Serial.println(" ms elapsed");
}

void loop() {
}

 で、これはSDカードに書き込むようになっている。

 私の手持ちの、ArduinoにSDカードをつなぐ手段は、先日から愛用中の「ETHERNET SHIELD 2」に搭載されているSDカードスロットだけだから、とりあえずこれを使う。

 手持ちのSDカードをETHERNET SHIELD 2に挿し、Adafruitのサイトのチュートリアルを参考に回路をブレッドボードに組む。

IMG_3122

 注意する点は2つだ。

  1.  サンプルスケッチをよく読むと、普通のSDカードは10番ピンを使うが、ETHERNET SHIELD 2を使う場合は4番ピンにつながる。なので、サンプルスケッチの中の「#define chipSelect 10」というところを「#define chipSelect 4」に書き換えなければならない。
  2.  メーカーサイトのチュートリアルでは、カメラに添付の10kΩの抵抗をTXの次に直列に二つ入れて、1本目と2本目の間からTXをとり、それをアースしているが、どうもこれだとうまく行かなかった。多分、このチュートリアルは給電が5Vだからだと思う。そこで、アースはそのままに、1本目の手前でTXをとるとうまくいった。
    IMG_3127

 そうやってArduinoをスタートさせると、写真が1枚だけ撮れる。

 下は、そうやって撮った私の顔である。

IMAGE02

 ……むっちゃむさくるしいなあw。

WIZnetでまた紹介され、嬉しい

投稿日:

 このところ凝っているArduinoでの遊び、そんなにお金もかからないし、何より楽しい。

 先日、私が作ったデジタルカメラのシャッターをネット経由で切る工夫が韓国「WIZnet」のサイトで紹介され、とても嬉しかった

 この「WIZnet」というところは私が買った「ETHERNET SHIELD 2」に搭載されているW5500というコントローラのメーカーなのだ。それで、私のブログにコメントをつけてくれ、サイトで紹介してくれたのであった。

 先日、安物の扇風機にWebインターフェイスを実装し、温度で制御する、という試みをやり、楽しかったので、「こんどはこんなのが出来ました~」とWIZnetのサイトに報告したら、それも紹介してあげましょう、と返事があって、このとおり紹介された。

 私の元の記事では「ウェブ扇風機」というダサい名前だったが、こういうふうに「Web controller for smart Fan」と書いてもらうと、なんだかカッコイイじゃないの(笑)。

 いやあ、Arduinoって、楽しいなあw。

ウェブ扇風機

投稿日:

 昨日試した扇風機遊びを少し進め、温度制御を付けてみたい。

 おあつらえ向きに、最初に買った「Seeedstudio SIDEKICK BASIC KIT」には、サーミスタが入っている。これで温度を検知して、温度ごとに扇風機の回転を制御するわけだ。値の変更などはETHERNET SHIELD 2をつなぎ、ウェブインターフェイスで行うと面白いだろう。

 以前にも試したが、しかし、このキット付属のサーミスタは情報が少なくて困る。そこで、ちゃんと計算してそれなりの温度測定をしてみたい。以前は測りたい温度近傍の特性値をテーブルから拾って2次式で近似しただけであった。

 スペックシートによれば、-55℃で10583.3Ω、+125℃で1.277Ωになる、とある。常温付近では0℃で190.07Ω、40℃で24.87Ωだ。もしサーミスタ直付けでArduinoから5V給電すれば、-55℃のときは0.47mAだが、0℃で26.3mA、40℃で201mAと、Arduinoに流せる電流の20mAを大きく逸脱してしまう。

 40度付近でも大丈夫なように、抵抗を付加しなければならない。40℃の時に流したい電流を5mAと仮定すれば、

IMG_3075

……というような計算で975Ωくらいつなげておけばよいということになる。

 しかし、手持ちの抵抗は、キットに入っていた330Ω・1kΩ・10kΩの3種類しかないから、これで賄うしかない。

 一番大きい抵抗は10kΩだから、これで計算しなおすと、40℃の時で

5V / (24.87Ω + 10kΩ) = 0.5mA

2番目は1kΩ、同じく40℃の時で

5V / (24.87Ω + 1kΩ) = 4.88mA

同様に3番目の330Ωだと

5V / (24.87Ω + 330Ω) = 14mA

…ということになる。間をとって、1kΩで回路を作ってみよう。

 次に、サーミスタは抵抗から温度を知る。一方、Arduinoは電圧からデジタル値を知る。したがって、電圧からまず抵抗値を知らなければその先の温度測定に進めない。

 先の電流調整用の1kΩ抵抗をR0として含め、現在のサーミスタの抵抗値を知るには、一般に式は次のようになろう。

IMG_3079

 これをソースコードに表せば、次のようになる。

  const float v0 = 5.0, r0 = 1000.0;  //  Arduino +5Vと電流調整抵抗1kΩ
  const int resolution = 1024;  //  アナログ入力の分解能
  int srcVal = 0;
  float vt = 0.0, rt = 0.0;
  
  srcVal = analogRead(THERMISTOR);
  vt = srcVal * (v0 / (resolution - 1));
  rt = (v0 * r0 - vt * r0) / vt;

 次に、知った抵抗値から温度を知る計算である。

 サーミスタの抵抗は対数特性を持っているので、関数は対数モデルになる。また、サーミスタの特性を表す重要な値は「B値」と呼ばれるものと、Ta(温度)、R0(抵抗)の3つで、それらはスペックシートに書いてある。

 TaとR0については、「温度がTaのとき、抵抗はR0になる」というものだ。私の手元のサーミスタは、スペックシートによると、

Ta = 25℃
R0 = 50kΩ

……とある。

 B値は「抵抗の対数と、温度の比」である。これもスペックシートに関係式が書いてある。

 これらのスペックシートの値と、知った抵抗値から温度を求める式は、次のようになる。

IMG_3080

 これを、先の抵抗値を知る部分と併せて、Arduinoで使える関数にすれば、このようになろうか。

float tempMesure(){
  const float B = 4350.0, Ta = 25.0, Rt0 = 50000.0;  //  MF11-503Kスペックシート記載
  const float K = 273.15;  //  熱力学温度の定数
  const float v0 = 5.0, r0 = 1000.0;  //  Arduino +5Vと電流調整抵抗1kΩ
  const int resolution = 1024;  //  アナログ入力の分解能
  int srcVal = 0;
  float vt = 0.0, rt = 0.0;
  
  srcVal = analogRead(THERMISTOR);
  vt = srcVal * (v0 / (resolution - 1));
  rt = (v0 * r0 - vt * r0) / vt;
  return(1.0 / (log(rt / Rt0) / B + 1.0 / (Ta + K)) - K);
}

 これを用いて、少し真面目っぽい温度測定スケッチを書けば、次のようになる。

//
//  thermistor2temp.ino
//    サーミスタでわりと真面目に温度を測る。
//    27.7.5(日)
//    佐藤俊夫
//
const int THERMISTOR = 1;

void setup() 
{ 
  Serial.begin(9600);
} 
 
void loop() 
{ 
  float t = 0.0;
  
  t = tempMesure();
  Serial.print("Temp = ");
  Serial.print(t);
  Serial.println("C");
  delay(500);
}

float tempMesure(){
  const float B = 4350.0, Ta = 25.0, Rt0 = 50000.0;  //  MF11-503Kスペックシート記載
  const float K = 273.15;  //  熱力学温度の定数
  const float v0 = 5.0, r0 = 1000.0;  //  Arduino +5Vと電流調整抵抗1kΩ
  const int resolution = 1024;  //  アナログ入力の分解能
  int srcVal = 0;
  float vt = 0.0, rt = 0.0;
  
  srcVal = analogRead(THERMISTOR);
  vt = srcVal * (v0 / (resolution - 1));
  rt = (v0 * r0 - vt * r0) / vt;
  return(1.0 / (log(rt / Rt0) / B + 1.0 / (Ta + K)) - K);
}

 次に、温度に応じて扇風機の回転数が変わるようにしよう。先日作った「ソリッドステート・リレーモジュール」を遺憾なく使用する。

ソリッドステート・リレーモジュール
IMG_3081

 このモジュールの内部には、秋月電子の「ソリッド・ステート・リレー(SSR)キット 25A(20A)タイプ」が組み付けてある。

 27℃で1/fゆらぎエフェクト、28℃で弱風、29℃で強風、とでもしてみようか。1/fゆらぎエフェクトには、先週書いた関数をそのままコピペする。

IMG_3083

//
//  thermistor2windFan.ino
//    サーミスタで温度を測り、扇風機を制御する。
//    27.7.5(日)
//    佐藤俊夫
//
const int THERMISTOR = 1, WINDFAN = 9;
void setup() 
{ 
  Serial.begin(9600);
  pinMode(WINDFAN, OUTPUT);
} 
 
void loop() 
{ 
  const int half = 128, full = 255;
  const float lowTemp = 27.0, midTemp = 28.0, highTemp = 29.0; 
  float t = 0.0, f = 0.0;
  
  t = tempMesure();
  Serial.print("temp=");
  Serial.println(t);
  if(t > highTemp){
    analogWrite(WINDFAN, full);
  }else
  if(t > midTemp){
    analogWrite(WINDFAN, half);
  }else
  if(t > lowTemp){
    f = f1Fluctuation();
    analogWrite(WINDFAN, f);
  }else{
    analogWrite(WINDFAN, 0);
  }
  delay(20);
}

float tempMesure(){
  const float B = 4350.0, Ta = 25.0, Rt0 = 50000.0;  //  MF11-503Kスペックシート記載
  const float K = 273.15;  //  熱力学温度の定数
  const float v0 = 5.0, r0 = 1000.0;  //  Arduino +5Vと電流調整抵抗1kΩ
  const int resolution = 1024;  //  アナログ入力の分解能
  int srcVal = 0;
  float vt = 0.0, rt = 0.0;
  
  srcVal = analogRead(THERMISTOR);
  vt = srcVal * (v0 / (resolution - 1));
  rt = (v0 * r0 - vt * r0) / vt;
  return(1.0 / (log(rt / Rt0) / B + 1.0 / (Ta + K)) - K);
}

int f1Fluctuation(){
  static float x = 0.1;
  if(x < 0.5){
    x = x + 2 * x * x;
  } 
  else {
    x = x - 2 * (1.0 - x) * (1.0 - x);
  }
  if(x < 0.05 || x > 0.995){
    x = random(10, 90) / 100.0;
  }
  return((int)(x * 255));
}

 回路図など、描くも愚かというか、こんな簡単なものである。

IMG_3084

 さて次に、ETHERNET SHIELD 2をつなぎ、この扇風機をWeb化する。昨日買っておいた「クリアランス確保用ピンソケット」が役に立つ。

IMG_3067

 組み付けるとこんな感じである。回路はネットにつながない場合と同じでよい。

IMG_3087

 こんなインターフェイスで動かす。

ネット扇風機インターフェイス

 設定した温度になると扇風機が回る。

IMG_3088

 スケッチはこのようになる。

//
//  web2thermistor_windFan.ino
//    サーミスタで温度を測り、扇風機を制御する。
//    27.7.5(日)
//    佐藤俊夫
//
#include <SPI.h>
#include <Ethernet2.h>
//
byte mac[] = {
  0x90, 0xA2, 0xDA, 0x0F, 0xF6, 0x74
};
IPAddress ip(192, 168, 1, 129);
EthernetServer SERVER(80);
EthernetClient CLIENT;
const int THERMISTOR = 1, WINDFAN = 9;
//
void setup() 
{ 
  Ethernet.begin(mac, ip);
  SERVER.begin();
  Serial.begin(9600);
  pinMode(WINDFAN, OUTPUT);
} 
 
void loop() 
{ 
  const int half = 128, full = 255;
  static float lowTemp = 27.0, midTemp = 28.0, highTemp = 29.0; 
  float t = 0.0, f = 0.0;
  //  Webサーバの動作
  char c;
  String rstr = "";
  CLIENT = SERVER.available();
  if (CLIENT) {
    while (CLIENT.connected()) {
      if (CLIENT.available()) {
        c = CLIENT.read();
        rstr += c;
        if(rstr.endsWith("\r\n")){
          break;
        }
      }
    }
    if(rstr.indexOf("low=") >= 0){
      lowTemp = rstr.substring(rstr.indexOf("low=") + 4, rstr.indexOf("&green=")).toInt();
      midTemp = rstr.substring(rstr.indexOf("mid=") + 4, rstr.indexOf("&blue=")).toInt();
      highTemp = rstr.substring(rstr.indexOf("high=") + 5, rstr.indexOf("&end")).toInt();
    }
    rstr = "";
    sendform(lowTemp, midTemp, highTemp);
    delay(1);
    // close the connection:
    CLIENT.stop();
  }
  // 温度測定
  t = tempMesure();
  Serial.print("temp=");
  Serial.println(t);
  if(t > highTemp){
    analogWrite(WINDFAN, full);
    Serial.println("high");
  }else
  if(t > midTemp){
    analogWrite(WINDFAN, half);
  }else
  if(t > lowTemp){
    f = f1Fluctuation();
    analogWrite(WINDFAN, f);
  }else{
    analogWrite(WINDFAN, 0);
  }
  delay(20);
}

float tempMesure(){
  //  Seeedstudio SIDEKICK BASIC KIT付属のサーミスタ「MF11-503K」で
  //  温度を測る。
  const float B = 4350.0, Ta = 25.0, Rt0 = 50000.0;  //  MF11-503Kスペックシート記載
  const float K = 273.15;  //  熱力学温度の定数
  const float v0 = 5.0, r0 = 1000.0;  //  Arduino +5Vと電流調整抵抗1kΩ
  const int resolution = 1024;  //  アナログ入力の分解能
  int srcVal = 0;
  float vt = 0.0, rt = 0.0;
  
  srcVal = analogRead(THERMISTOR);
  vt = srcVal * (v0 / (resolution - 1));
  rt = (v0 * r0 - vt * r0) / vt;
  return(1.0 / (log(rt / Rt0) / B + 1.0 / (Ta + K)) - K);
}

int f1Fluctuation(){
  //  間欠カオス法により0~255の間で1/fゆらぎを生成して返す。
  static float x = 0.1;
  if(x < 0.5){
    x = x + 2 * x * x;
  } 
  else {
    x = x - 2 * (1.0 - x) * (1.0 - x);
  }
  if(x < 0.05 || x > 0.995){
    x = random(10, 90) / 100.0;
  }
  return((int)(x * 255));
}

void sendform(float lowTemp, float midTemp, float highTemp){
  //  フォームを送る。
  char* formFirstHalf[] = {
    "<html>",
    "  <head>",
    "    <meta charset=\"utf-8\">",
    "  </head>",
    "  <body bgcolor='#ddddff'>",
    "    <center>",
    "      <h1>Arduino ネット扇風機</h1>",
    "      <form method='GET'>",
    "        <table>",
    "          <tr>",
    "  	    <th>ゆらぎ送風温度</th>",
    "  	    <th>弱風温度</th>",
    "  	    <th>強風温度</th>",
    "	  </tr>"
  };  //  14 num.
  char* formSecondHalf[] = {
    "        </table>",
    "        <input type='hidden' name='end'>",
    "        <input type='submit' value='セット'>",
    "      </form>",
    "    </center>",
    "  </body>",
    "</html>"
  };  // 7 num.
  int i = 0;

  CLIENT.println("HTTP/1.1 200 OK");
  CLIENT.println("Content-Type: text/html");
  CLIENT.println("Connection: close");
  CLIENT.println();
  CLIENT.println("<!DOCTYPE HTML>");
  for(i = 0; i < 14; i++){
    CLIENT.println(formFirstHalf[i]);
  }
  CLIENT.println("          <tr>");
  CLIENT.println("            <td>");
  CLIENT.println("           <input type='text' name='low' size='6em' value =");
  CLIENT.print(lowTemp);
  CLIENT.println(">");
  CLIENT.println("            </td>");
  CLIENT.println("            <td>");
  CLIENT.println("           <input type='text' name='mid' size='6em' value =");
  CLIENT.print(midTemp);
  CLIENT.println(">");
  CLIENT.println("            </td>");
  CLIENT.println("            <td>");
  CLIENT.println("           <input type='text' name='high' size='6em' value =");
  CLIENT.print(highTemp);
  CLIENT.println(">");
  CLIENT.println("            </td>");
  CLIENT.println("          </tr>");
  for(i = 0; i < 7; i++){
    CLIENT.println(formSecondHalf[i]);
  }

}






秋葉原路上

投稿日:

 今日、知人から聞いた秋葉原の人気ラーメン店「野郎ラーメン」というところへ行った。

 私がラーメン屋に行くのは、実は最近では珍しい。北海道で暮らした頃に食べたラーメンがうまかったので、その後、他のラーメンがあまりうまいと感じられなくなったということも一因である。また、妻が作る具入りのインスタントラーメンのほうがうまいと感じてしまうので、ラーメン店から遠ざかる、ということもある。

 さておき、ラーメン好きの人に言わせると、その「野郎ラーメン」という店は、「二郎インスパイア系ラーメン店」という系列に分類されるそうで、いやはや、ラーメン好きの人たちの旺盛な探求欲には恐れ入るばかりである。

 野郎ラーメンでは、その名も「豚野郎ラーメン」というファッキンなネーミングのメニューが人気だそうだが、今日は味付卵入りの「野郎ラーメン」というのを選んだ。

 小柄な白人の女の子が片言でフロアを切り盛りしており、後れ毛がなにやらムフフと可愛らしい。

 ラーメンを食って満腹し、店の前でSwarmでも入れておこうかい、とスマホをいじっていたら、

「すいません、フジテレビです。取材なんですが」

と、お兄さんがアンケート用紙と鉛筆を持って近寄ってきた。

「はいはい、なんでしょう?」

「今、秋葉原は『カレー激戦区』ということで世間の噂が盛り上がっているんです。」

「へっ?……はあ、そうなんですか?」という私のスットボケた返事に、お兄さんは(あれっ、これはインタビュー相手を間違ったな)という顔をしたが、あきらめず、

「で、もし、おすすめのカレー店や噂のカレー店などをご存知でしたら教えていただけませんか?」

「えっ……。す、すみません、ごめんなさい。カレー、あんまり食べないもので……」

「……失礼しました、ハハッ、最初の反応で、そうだろうなあ、と思いました、いや、すみません。……今日はラーメンで?」

「ええ、それがね、ラーメンもめったに食わないんですがね。今日は噂をきいて入ってみたんですよ。」

「へえ、めったに召し上がらない?」

「秋葉原へ来ると、万世橋の向う、連雀町のほうの、じじむさい界隈のじじむさいものを食う方が性に合ってましてね」

「ははあ、それはなるほど、粋向きですねえ。……いやどうも、お時間をお取らせしました。よい一日で」

……などと、お兄さん、なかなか軽妙に人を喋らせるのであった。

 本郷通りを南へ横切って、メイドさんの並んでいるカルチャーズ・ゾーンの前を通り過ぎ、千石電商へ行ってネジ、ケース、ニッパー、蛇の目基盤、ピンコネクタなどを買い込む。

 今日の買い物の大ヒットは、このピンコネクタである。180円。

IMG_3067

 こんなものの何が大ヒットなのかと言うと、これは実はArduino用のセットなのである。本来はArduino製作用のユニバーサル基盤に半田付けして使うものだ。しかし、私がこれを購入したのは別の理由だ。

 これは、先日買ったArduino用のブレッドボードシールドである。

IMG_3063

 まことに便利なのだが、難点がある。ETHERNET SHIELD 2 とともに使おうとすると、ピンの長さが短く、ETHERNET SHIELD 2 の RJ45コネクタのシールドと干渉してしまい、下の写真のように斜めになってきちんと挿さらないのである。

IMG_3065

 そこで、買ってきたこのピンコネクタを、ETHERNET SHIELD 2 に挿す。

IMG_3068

 こうすることによりめでたくArduino UNO R3 + ETHERNET SHIELD 2 + ブレッドボードシールドの3階建てがまっすぐに挿さって、使えるようになるのであった。

IMG_3069

 めでたし、めでたし。