国内IPのみからsshを受け付ける設定(Fedora firewalld)

自宅なりレンタルなり、自前でサーバーを立ち上げて公開するとあっという間に世界中からアタックがやってきます。
うちでは会社や出先からちょっと設定をいじったりソースを取り出したりしたいなという時のために自宅鯖のsshを開けていて、
アタックがあったらメールでお知らせみたいな運用をしばらくはしていたのですが、多すぎて辟易してしまいました。
アタック元を見るとほとんど海外、とくにcn,kr,twあたりというのが実情でありまして、
sshを開けている目的からすると365日全世界にオープンにしておく必要はないので今は国内IP以外はfirewallではじいています。

そんなわけで、今回もまたFedoraのfirewalldネタとして書いてみることにします。

(実際は、自分だけしか使わないなら例えばポート変えちゃってもいいんですけどね)

国内IPの判別

世界の国別 IPv4 アドレス割り当てリスト http://nami.jp/ipv4bycc/
で使いやすく整理していただいているものをありがたく使わせて頂きます。
というかここでやっていることが上記サイトの例の応用にすぎません。

firewalldによる特定IPのアクセプト/リジェクト

細かい設定は、firewall-cmd --direct を通じて行います。オプションはiptablesと同様です。
例)

firewall-cmd --direct --add-rule ipv4 filter INPUT 1 -m conntrack --ctstate NEW -m tcp -p tcp --dport 22 -s 192.168.0.0/16 -j ACCEPT

方針

22番ポートに関して、以下の方針でfirewallを組みます。

  • 基本ポリシーは、REJECT
  • ローカルネットワークのIP(192.168.0.0)はACCEPT
  • 国内IPはACCEPT

スクリプト

上記方針を実現するため、 /usr/local/sbin/allow-ssh-from-jp.sh を以下の内容で作成します。

#!/bin/sh

WORK_DIR=/var/firewall

verbose_exec()
{
        echo $*
        $*
}

#
# cd to working dir
#
if [ ! -d $WORK_DIR ]; then
        mkdir -p $WORK_DIR
fi
cd $WORK_DIR

#
# if -dl option is set, download cidr.txt.gz
#
if [ "$1" = "-dl" ]; then
        if [ -f cidr.txt.gz ]; then
                mv cidr.txt.gz cidr-old.txt.gz
        fi
        wget http://nami.jp/ipv4bycc/cidr.txt.gz
        if [ $? -ne 0 ]; then
                rm cidr.txt.gz
        fi
fi

if [ ! -f cidr.txt.gz ]; then
        echo cidr.txt.gz not found.
        exit 1
fi

#
# remove all current rules regarding port 22
#
firewall-cmd --direct --get-rules ipv4 filter INPUT | grep '\--dport 22' | while read rule; do
        verbose_exec firewall-cmd --direct --remove-rule ipv4 filter INPUT $rule
done

#
# accept local network
#
verbose_exec firewall-cmd --direct --add-rule ipv4 filter INPUT 1 -m conntrack --ctstate NEW -m tcp -p tcp --dport 22 -s 192.168.0.0/16 -j ACCEPT

#
# reject all (priority=3)
#
verbose_exec firewall-cmd --direct --add-rule ipv4 filter INPUT 3 -m conntrack --ctstate NEW -m tcp -p tcp --dport 22 -j REJECT

#
# accept from JP (priority=2)
#
zcat cidr.txt.gz | sed -n 's/^JP\t//p' | while read address; do
        verbose_exec firewall-cmd --direct --add-rule ipv4 filter INPUT 2 -m conntrack --ctstate NEW -m tcp -p tcp --dport 22 -s $address -j ACCEPT
done

IPの割り当て状況は変わりますので、このスクリプトを定期的に実行してfirewallを更新していく必要があると思いますが、
リスト提供元のサーバ側に迷惑のかからないように頻度にはご注意ください。
また、スクリプト実行に結構な時間がかかります(手元で数分程度)。起動スクリプトに組み込む場合はバックグラウンドで実行する等の工夫が必要です。

Raspberry Pi でI2C: 温度センサーを使う

Raspberry PiでI2Cデバイスをいくつか使ってみたので、何回かに分けて紹介したいと思います。

まずは秋月のADT7410を使用した温度センサーモジュールを接続し、温度データを読めるようになるまでの道のりを紹介します。
ただしこのデバイスRaspberry Piとは相性が悪く、ネイティブのI2Cドライバを使用したアクセスにはかなりの制限が避けられませんでした。
しかし回避策も一応存在しますのでそれも合わせて紹介します。

準備

バイス接続

GND, VDD(3.3V), SCL, SDA を接続します。Raspberry Pi のピンは以下のものを使用します。

とりあえず接続だけした状態です。
配線については追々きれいにしていきます。
下の方に伸びて緑色のスイッチにつながっている2本の線はシャットダウンスイッチなので今回は関係ありません。

センサーモジュールのボード上でSCL, SCAのプルアップができますが、
プロセッサ内でプルアップされているようなので(データシート確認していませんが)
バイス側では不要のようです。

また同様にI2Cスレーブアドレス(デフォルト0x48)の変更もできますので必要に応じてパッドをショートさせて設定してください。

カーネルモジュール

i2c-dev と、依存するモジュールとして i2c-bcm2708 が必要ですが、
i2c-bcm2708 はblacklist.confに入っていて自動ではロードしないようになっていますので、これを解除します。

/etc/modprobe.d/raspi-blacklist.conf
blacklist i2c-bcm2708 をコメントアウト

# blacklist spi and i2c by default (many users don't need them)

blacklist spi-bcm2708
#blacklist i2c-bcm2708

その上で、i2c-devをロードします。

% sudo modprobe i2c-dev

起動時に自動でロードさせたい場合は /etc/modules に i2c-dev を追加します。

必要パッケージのインストール

i2c-toolsをインストールします。

% sudo apt-get install i2c-tools

以上で準備は完了です。

バイスへのアクセス確認

バイス検出

i2cdetectでデバイスを検出してみます。コマンドラインオプション最後の'1'はI2Cバス番号ですが、
Raspberry Piのハードウェアリビジョンによって0か1か変わりますので注意してください。
(R1:0, R2:1)

% sudo /usr/sbin/i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- -- 

I2Cアドレス0x48になにかいることがわかります。

レジスタread

ADT7410のデータシートによると、温度データは2byteでレジスタアドレス0x00がMSB, 0x01がLSB(Big Endian的な配置)です。
i2cgetを使ってアドレス0x00からwordアクセスしてみます。(後述しますが、0x00, 0x01へのバイトアクセス2回では動きませんのでご注意)

% sudo /usr/sbin/i2cget -y 1 0x48 0x00 w
0x180d

これをバイトスワップして(9.7)フォーマット(signed)で評価すると温度データ(摂氏)になります。
バイトスワップ→0x0d18
(9.7)で評価→26.1875(度C)

問題点

「0x00, 0x01へのバイトアクセス2回では動きません」と書きましたが、実際それを行うと以下のように0x00,0x01で全く同じデータが観測されてしまいます。

% sudo /usr/sbin/i2cget -y 1 0x48 0x00 b
0x0d
% sudo /usr/sbin/i2cget -y 1 0x48 0x01 b
0x0d

またi2cdumpで全レジスタを読み出してみても、以下のように明らかにおかしなデータとなります。

% sudo /usr/sbin/i2cdump -y 1 0x48 b
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f    0123456789abcdef
00: 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c    ????????????????
10: 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c    ????????????????
20: 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 00    ???????????????.
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 0c 0c 0c    .............???

読み出し単位を変えると少し変化しますが、最大でも4byteの繰り返しです。

% sudo /usr/sbin/i2cdump -y 1 0x48 i
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f    0123456789abcdef
00: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
10: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
20: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
30: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
40: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
50: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
60: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
70: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
80: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
90: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
a0: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
b0: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
c0: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
d0: 0c 58 80 00 0c 58 80 00 0c 58 80 00 0c 58 80 00    ?X?.?X?.?X?.?X?.
e0: 0c 50 80 00 0c 50 80 00 0c 50 80 00 0c 50 80 00    ?P?.?P?.?P?.?P?.
f0: 0c 50 80 00 0c 50 80 00 0c 50 80 00 0c 50 80 00    ?P?.?P?.?P?.?P?.

これはどうやらADT7410がI2CのRepeated Start Conditionという動作を要求する一方で、
Raspberry PiのプロセッサBCM2835のI2Cモジュールがそれに非対応ということが原因のようです。

きちんと理解していませんが、Stop Conditionを挟むとアドレスがリセットされてしまう、とかそんな感じ?でしょうか

これはもうハードウェアの問題なのでソフトウェアではどうしようもありません。
以下のサイトでは外付けロジックでSCLの論理を変えちゃおうという試みについて書かれていますのでどうしてもという方は参照してください。
http://www.circuitwizard.de/raspi-i2c-fix/raspi-i2c-fix.html

HiPi (I2C bit banging)

上記問題の1つの解決方法が、perlのHiPiというモジュールを使う方法です。
これはハードウェアのI2C機能を使わず、SCL,SDAピンをGPIOとして扱って全てソフトウェア的にロジックをコントロールしてしまおうというものです(この手のものをbit bangingと呼ぶようです)。
当然のことながらハードウェアコントロールに比べるとむちゃくちゃ遅いのですが、
それでもこちらはRepeated Start Conditionに対応しているのでとりあえず任意のアドレスにアクセスできます。

以下の手順でインストールします。

% perl -MCPAN -e 'install "LWP:Simple"'
% wget http://raspberry.znix.com/hipifiles/hipi-install
% perl hipi-install

アドレス0x00,0x01を読んでみます。

% sudo hipi-i2c r 1 0x48 0x00 1
12
% sudo hipi-i2c r 1 0x48 0x01 1
96

それらしい値が取得できました。(繰り返しますが、むちゃくちゃ遅いです)

またi2c-toolsではどうやっても読めなかった、アドレス0x0b(チップのID)を読んでみます。

% sudo hipi-i2c r 1 0x48 0x0b 1
203

データシートによると 0b11001xxx(0xc8-0xcf)が読めるはずですので、203=0xcbということでOKです。

幸いなことに、頻繁にアクセスする温度データは0x00-0x01にあるため
i2c-toolsのwordアクセス(ひいてはnativeドライバのAPI)で読み出せますし、
設定を変えるため諸々のレジスタにアクセスする場合はHiPiを使用することができるため、
結果的には一応フル機能使えるということになりそうです。

まとめ

以上でI2C温度センサーが使用できるようになりました。
一口にI2Cと言っても本デバイスのように相性が悪いものもあるみたいですが、この手の工作は多少の困難があった方が楽しいですよね (^^;

おまけ

ADT7410は温度スレッショルドを設定して割り込みを出す機能がありますので、
チップからRaspberry PiのGPIOに配線すればそのような機能も使うことができると思います。

Fedoraのfirewalldの設定

Fedora18以降、ファイアウォール・ルーティングを行うサービスがiptablesの代わりにfirewalldがデフォルトになっています。iptablesも残っていますのでそちらを使っても良いのですが、firewalldの方がいろいろ整理されて設定も(覚えてしまえば)やりやすいように感じます。
ぐぐるさんの方でもまだあまり実例がヒットしないので、うちの設定をシェアしてみます。

Fedoraのドキュメント(日本語)
https://fedoraproject.org/wiki/FirewallD/jp

ネットワークの接続状態と、方針

以下にネットワーク構成を示します。

p2p1: 宅内ネットワーク
p4p1: 外向きネットワーク(プロバイダ支給のルーターへ)

現在の構成上、p4p1の先に別途ルーターがいますが、元々は直接インターネット接続することを意識した形です。テレホーダイ時代からずっとこのサーバ兼ルータなマシンを立てる構成でやってます。

外部(Internet)に提供するサービス

内部(宅内)に提供するサービス

  • 全ポート開放(走ってるサービスはdns, dhcp, nfs, smbとかその他もろもろ)
  • IP masquerade

サービスの有効化・起動

Fedora18ではfirewalldがデフォルトなのでfirewalld.serviceが有効になっているはずですが、そうでない場合は有効にします。

# systemctl enable firewalld.service

また、今すぐ起動する場合は以下のようにします。

# systemctl start firewalld.service

設定の確認

SELinuxはdisabledです

まずはfirewalldが動作中であることを確認します。

# firewall-cmd --state && echo "Running" || echo "Not running"
Running

全ゾーンの状態を確認してみます。何も設定していなければ、以下のような状態になると思います。

# firewall-cmd --list-all-zones
drop
  interfaces:
  services:
  ports:  
  forward-ports:
  icmp-blocks:

work
  interfaces:
  services: ipp-client mdns dhcpv6-client ssh
  ports:  
  forward-ports:
  icmp-blocks:

internal
  interfaces:
  services: ipp-client mdns dhcpv6-client ssh samba-client
  ports:
  forward-ports:
  icmp-blocks:

external
  interfaces:
  services: ssh
  ports:
  forward-ports:
  icmp-blocks:

trusted
  interfaces:
  services:
  ports:
  forward-ports:
  icmp-blocks:

home
  interfaces:
  services: ipp-client mdns dhcpv6-client ssh samba-client
  ports:
  forward-ports:
  icmp-blocks:

dmz
  interfaces:
  services: ssh
  ports:
  forward-ports:
  icmp-blocks:

public
  interfaces: p4p1 p2p1
  services: mdns dhcpv6-client ssh
  ports:
  forward-ports:
  icmp-blocks:

block
  interfaces:
  services:
  ports:
  forward-ports:
  icmp-blocks:

ローカル側I/F(p2p1)の設定

ゾーンをtrusted(全コネクション受け入れ)にする

# firewall-cmd --zone=trusted --change-interface=p2p1

恒久的に行う場合は、ifcfgスクリプトに ZONE=trusted を追加します。
/etc/sysconfig/network-scripts/ifcfg-p2p1

TYPE=Ethernet
BOOTPROTO=none
DEFROUTE=yes
IPV4_FAILURE_FATAL=yes
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
NAME=p2p1
UUID=2559a856-f4d9-4a20-8634-c924973edb01
ONBOOT=yes
IPADDR0=192.168.1.1
PREFIX0=24
DNS1=192.168.1.1
DOMAIN=example.com
HWADDR=54:04:A6:6B:91:53
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
ZONE=trusted

外部向けI/F(p4p1)の設定

ゾーンをexternalにする

# firewall-cmd --zone=external --change-interface=p4p1

同様に、ifcfgスクリプトに ZONE=external を追加します。
/etc/sysconfig/network-scripts/ifcfg-p4p1

TYPE=Ethernet
BOOTPROTO=none
DEFROUTE=yes
IPV4_FAILURE_FATAL=yes
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
NAME=p4p1
UUID=9584eeda-1d24-40c1-a66f-005e25e9d9c2
ONBOOT=yes
IPADDR0=192.168.0.192
PREFIX0=24
GATEWAY0=192.168.0.1
DNS1=192.168.0.1
HWADDR=00:13:3B:0D:D0:E1
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
ZONE=external

外部向けのサービスを提供するポートをオープンします。
sshはデフォルトで開いているので、その他に http, smtp, pop3 ポートを開けます。ランタイム変更と、永続的変更(--permanent)がそれぞれ必要です。

# firewall-cmd --zone=external --add-service=http
# firewall-cmd --zone=external --add-service=http --permanent
# firewall-cmd --zone=external --add-service=smtp
# firewall-cmd --zone=external --add-service=smtp --permanent
# firewall-cmd --zone=external --add-port=110/tcp
# firewall-cmd --zone=external --add-port=110/tcp --permanent

IPマスカレード設定

(2013.11.20修正: --add-msqueradeするのは内側でなくて外側のゾーンのようでした。訂正します)
カーネルのIPフォワードを有効にし、外側のzoneに --add-masquerade 設定を追加します。

# echo 1 > /proc/sys/net/ipv4/ip_forward

Fedora 18の場合、上記を恒久的に設定する場合は /usr/lib/sysctl.d/00-system.conf の以下の設定を0から1に変更。

net.ipv4.ip_forward = 1

firewalld設定

# firewall-cmd --zone=external --add-masquerade
# firewall-cmd --zone=external --add-masquerade --permanent

設定の確認

変更したzoneの設定を確認してみます。

# firewall-cmd --list-all --zone=trusted
trusted
  interfaces: p2p1
  services: 
  ports: 
  forward-ports: 
  icmp-blocks: 
# firewall-cmd --list-all --zone=external
external
  interfaces: p4p1
  services: smtp http ssh
  ports: 110/tcp
  forward-ports: 
  icmp-blocks: 

コマンドで設定した内容が、/etc/firewalld/ 以下の設定ファイルに保存されていますのでチラ見してみます。
/etc/firewalld/zones/external.xml

<?xml version="1.0" encoding="utf-8"?>
<zone>
  <short>External</short>
  <description>For use on external networks. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description>
  <service name="http"/>
  <service name="smtp"/>
  <service name="ssh"/>
  <port protocol="tcp" port="110"/>
  <masquerade enabled="Yes"/>
</zone>

おわりに

とりあえず基本的な設定はこんなところですが、sshブルートフォースアタック対策とかもう少しこまごました設定もする予定なので、続編があるかもしれません。

Raspberry Pi をJenkinsのスレーブノードにする

Raspberry Pi をJenkinsのスレーブノードとして追加する手順をまとめました。
クロスプラットフォームのソフトウェア開発においてARM-Linux版の動作確認などに使用できます。

前提

LinuxのPCなどにJenkinsのサーバが構築されていることが前提です。
手元では、Jenkinsのバージョンは1.536です。
また、必須ではありませんがJenkinsのグローバルセキュリティ設定はしておいた方がよいと思います。

Javaのインストール

参考(というかこのまんまですが)
http://raspberrypi.stackexchange.com/questions/4683/how-to-install-java-jdk-on-raspberry-pi

https://jdk8.java.net/download.html
から ARM-Linux用のJDKをダウンロード&展開、インストールします。

% sudo tar xvf jdk-8-ea-b36e-linux-arm-hflt-*.tar.gz -C /opt
jdk1.8.0/COPYRIGHT
jdk1.8.0/LICENSE
jdk1.8.0/README.html
jdk1.8.0/THIRDPARTYLICENSEREADME.txt
jdk1.8.0/bin/
jdk1.8.0/bin/jinfo
 :
% sudo update-alternatives --install "/usr/bin/java" "java" "/opt/jdk1.8.0/bin/java" 1
% java -version
java version "1.7.0_40"
Java(TM) SE Runtime Environment (build 1.7.0_40-b43)
Java HotSpot(TM) Client VM (build 24.0-b56, mixed mode)

上記で、Jenkinsではjavaコマンドだけで充分ですが、javaコマンド以外(javac等)も必要に応じて
update-alternatives を使用してインストールしておきます。

スレーブノードの設定

動作中のJenkinsから
[jenkins] -> [Jenkinsの管理] -> [ノードの管理] -> [新規ノード作成]
を選択し、以下の例のようにノードを作成します。

これでノードが作成されました。(もちろんまだコネクトされていません)

作成したRaspberryPiノードをクリックすると、以下の画面になります。

この中で

・スレーブでコマンドラインから起動:
java -jar slave.jar -jnlpUrl http://<host:port>/computer/RaspberryPi/slave-agent.jnlp -secret 5796...

というインストラクションがあるので、この方法でRaspberry Piからスレーブエージェントを起動します。
-secret ... のオプション部分はJenkinsでグローバルセキュリティ設定をしていなければ出ないと思います。

上記画面において slave.jar のところはリンクになっており、リンクURLをコピーしてRaspberry側からwgetしてセーブしておきます。

% wget http://<host:port>/jnlpJars/slave.jar
--2013-10-29 01:23:31--  http://<host:port>/jnlpJars/slave.jar
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 346309 (338K) [application/java-archive]
`slave.jar' に保存中

100%[==============================================>] 346,309     --.-K/s 時間 0.09s   

2013-10-29 01:23:31 (3.83 MB/s) - `slave.jar' へ保存完了 [346309/346309]

そして画面表示の通りにコマンドを起動すればJenkinsのノードとしてコネクトされます。
ただしヒープサイズのデフォルト値が結構小さく、メモリを食うようなテストを走らせたときにヒープ不足で落ちることがありましたので
解決策として -Xms, -Xmx を指定しました。

% java \
  -Xms128m -Xmx448m \
  -jar slave.jar \
  -jnlpUrl http://<host:port>/computer/RaspberryPi/slave-agent.jnlp \
  -secret 5796...
10 29, 2013 1:24:45 午前 hudson.remoting.jnlp.Main$CuiListener <init>
情報: Jenkins agent is running in headless mode.
10 29, 2013 1:24:46 午前 hudson.remoting.jnlp.Main$CuiListener status
情報: Locating server among [http://<host:port>/]
10 29, 2013 1:24:46 午前 hudson.remoting.jnlp.Main$CuiListener status
情報: Connecting to <host>:34774
10 29, 2013 1:24:46 午前 hudson.remoting.jnlp.Main$CuiListener status
情報: Handshaking
10 29, 2013 1:24:47 午前 hudson.remoting.jnlp.Main$CuiListener status
情報: Connected

ノード用のワークスペースを作成

ノード作成時に「リモートFSルート」という項目に "/home/pi/jenkins" と設定しました。
これはスレーブノードがワークスペースとして使用するディレクトリになります。
プロジェクト実行前にこのディレクトリを作成しておく必要があります。

% mkdir /home/pi/jenkins

スレーブエージェントは起動したユーザーの権限で動作しますので、
このディレクトリには当該ユーザーが読み書きできるようなパーミッション設定をしておいてください。

Jenkinsプロジェクト設定

特定のノードで実行したいプロジェクトを作る場合は、プロジェクト設定の中で
「実行するノードを制限」のチェックボックスにチェックを入れ、
ラベル式に実行させたいノードのラベルにマッチするような式を書きます。

今回の例で言うと、Raspberry Piのノードのラベルを "ARM-Linux" としましたので
ラベル式にそのまま "ARM-Linux" と入れればOKです。

あとは自由にプロジェクトを作成し、実行するだけです。
以下は適当に作ったサンプルプロジェクトの出力です。
RaspberryPi ノードで実行されたことが確認できます。

おわりに

Raspberry PiをJenkinsのスレーブノードとして動作させるにはそれほど複雑な作業は必要ありませんでしたので、
手軽なARMのテストプラットフォームとしても活用しやすいのではないでしょうか。

Raspberry Pi のGPIOをいじる

Raspberry Pi にシャットダウンボタンをつける というエントリではGPIOを使ってシャットダウンボタンを作りましたが、
pythonのライブラリを使ってスクリプトを書いただけでローレベルのAPIがどうなっているかはノータッチでした。
ただ、一般ユーザー権限で走らせると /dev/mem にアクセスできないというエラーが出るのでそこを叩いているということは分かりました。
つまりきちんとしたドライバのAPIではなくって、メモリ空間にマップされたレジスタを直接いじってるっぽいんですよね。
いやそれはちょっとイカンでしょうと思いますので、もう少しまともなAPIでコントロールできるようにいじってみました。

LinuxのGPIOインタフェース

カーネルソースの Documentation/gpio.txt に一通り書いてありますが、ユーザーランドから設定を動的にいじるためにはsysfsを使用します。
/sys/class/gpio というディレクトリを見ると、以下のようなエントリがあります。

% sudo ls -l /sys/class/gpio
合計 0
-rwxrwx--- 1 root gpio 4096  1月  1  1970 export
lrwxrwxrwx 1 root gpio    0  1月  1  1970 gpiochip0 -> ../../devices/virtual/gpio/gpiochip0
-rwxrwx--- 1 root gpio 4096  1月  1  1970 unexport

ここでexportというエントリにコントロールしたいGPIO番号を書き込むとそのGPIOに関するエントリが作成されます。

% sudo sh -c 'echo 23 > /sys/class/gpio/export'  
% sudo ls -l /sys/class/gpio
合計 0
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 export
lrwxrwxrwx 1 root gpio    0 10月  6 22:47 gpio23 -> ../../devices/virtual/gpio/gpio23
lrwxrwxrwx 1 root gpio    0  1月  1  1970 gpiochip0 -> ../../devices/virtual/gpio/gpiochip0
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 unexport
% sudo ls -l /sys/class/gpio/gpio23/
合計 0
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 active_low
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 direction
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 edge
drwxrwx--- 2 root gpio    0 10月  6 22:47 power
lrwxrwxrwx 1 root gpio    0 10月  6 22:47 subsystem -> ../../../../class/gpio
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 uevent
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 value

これらのsysfsエントリを使ってinput/outputの設定や割り込みのエッジ設定等はできるのですが、
pullup/pulldownを設定するインタフェースがありません。
カーネルのコード(https://github.com/raspberrypi/linux/tree/rpi-3.6.y)を確認しても
pullup/pulldownをコントルールするGPPUD,GPPUDCLKnレジスタを扱っているらしきコードは見つかりませんでした。

カーネル修正

もちろん外部回路でプルアップすることもできるのですが、せっかくチップの機能があるのでそれを使いたいと思います。
(曲がりなりにも /dev/mem をいじれば使える訳ですし)
というわけでカーネルを少しいじって、これを扱えるようなsysfsエントリを追加してみました。

解説は省略しますが、以下にコードを置きました。
https://github.com/penkoba/linux-rpi/tree/rpi-3.6.y-gpio-pullud

このカーネルを使って起動し、先ほどと同じ手順を行うと、以下のようにpulludエントリが追加されています。

% sudo sh -c 'echo 23 > /sys/class/gpio/export'  
% sudo ls -l /sys/class/gpio/gpio23/
合計 0
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 active_low
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 direction
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 edge
drwxrwx--- 2 root gpio    0 10月  6 23:10 power
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 pullud
lrwxrwxrwx 1 root gpio    0 10月  6 23:10 subsystem -> ../../../../class/gpio
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 uevent
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 value

ここに "up", "down", "none" を書き込むことによって内部のpullup/downをコントロールできます。
もちろん現在の状態の読み出しもできます。

アプリケーション

このsysfsエントリを使って、GPIOの割り込み待ちをするテストアプリケーションを書いてみます。
少し長いですが、以下に全リストを示します。

gpio-irq-demo.c

/*
 *            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *                    Version 2, December 2004
 * 
 * Copyright (C) 2013 penkoba
 *
 * Everyone is permitted to copy and distribute verbatim or modified
 * copies of this license document, and changing it is allowed as long
 * as the name is changed.
 *
 *            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 *
 *  0. You just DO WHAT THE FUCK YOU WANT TO.
 */

#include <stdio.h>
#include <poll.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/signal.h>

static volatile int signal_received = 0;

static void sigint_handler(int signo)
{
        printf("signal %d received.\n", signo);
        signal_received = 1;
}

static void setup_signal(void)
{
        struct sigaction sigt;

        sigt.sa_handler = sigint_handler;
        __sigemptyset(&sigt.sa_mask);
        sigt.sa_flags = SA_NODEFER | SA_RESETHAND;
        sigt.sa_restorer = NULL;
        sigaction(SIGINT, &sigt, NULL);
}

int get_gpio_fn(char *fn, int gpio_nr, const char *basename)
{
        return sprintf(fn, "/sys/class/gpio/gpio%d/%s", gpio_nr, basename);
}

int simple_write_file(const char *fn, const char *s)
{
        int fd;

#if 0
        printf("writing %s > %s\n", s, fn);
#endif
        fd = open(fn, O_WRONLY);
        if (fd < 0) {
                perror(fn);
                return -1;
        }
        if (write(fd, s, strlen(s)) < 0) {
                perror(fn);
                return -1;
        }
        close(fd);

        return 0;
}

int setup(int gpio_nr)
{
        char fn[64];
        struct stat st;

        printf("configuring GPIO %d as\n"
               "  input, interrupt on falling edge, internal pull-up\n",
               gpio_nr);

        sprintf(fn, "/sys/class/gpio/gpio%d", gpio_nr);
        if (stat(fn, &st) < 0) {
                char nr_str[16];
                sprintf(nr_str, "%d", gpio_nr);
                if (simple_write_file("/sys/class/gpio/export", nr_str) < 0)
                        return -1;
        }

        get_gpio_fn(fn, gpio_nr, "direction");
        if (simple_write_file(fn, "in") < 0)
                return -1;

        get_gpio_fn(fn, gpio_nr, "edge");
        if (simple_write_file(fn, "falling") < 0)
                return -1;

        get_gpio_fn(fn, gpio_nr, "pullud");
        if (simple_write_file(fn, "up") < 0)
                return -1;

        return 0;
}

void teardown(int gpio_nr)
{
        char fn[64];
        char nr_str[16];

        printf("clean up GPIO %d\n", gpio_nr);

        get_gpio_fn(fn, gpio_nr, "edge");
        if (simple_write_file(fn, "none") < 0)
                return;

        get_gpio_fn(fn, gpio_nr, "pullud");
        if (simple_write_file(fn, "none") < 0)
                return;

        sprintf(nr_str, "%d", gpio_nr);
        if (simple_write_file("/sys/class/gpio/unexport", nr_str) < 0)
                return;
}

int main(int argc, char **argv)
{
        char fn[64];
        int fd, ret;
        int gpio_nr;
        struct pollfd pfd;
        char rdbuf[5];

        if (argc != 2) {
                printf("Usage: %s <GPIO>\n", argv[0]);
                return 1;
        }
        gpio_nr = atoi(argv[1]);

        if (setup(gpio_nr) < 0)
                return 1;

        setup_signal();

        get_gpio_fn(fn, gpio_nr, "value");
        fd = open(fn, O_RDONLY);
        if (fd < 0) {
                perror(fn);
                return 1;
        }
        pfd.fd = fd;
        pfd.events = POLLPRI;
        ret = read(fd, rdbuf, sizeof(rdbuf));
        if (ret < 0) {
                perror(fn);
                return 1;
        }

        for (;;) {
                lseek(fd, 0, SEEK_SET);
                ret = poll(&pfd, 1, -1);
                if (signal_received)
                        break;
                if (ret < 0) {
                        perror("poll()");
                        close(fd);
                        return 1;
                }
                if (ret == 0) {
                        printf("timeout\n");
                        continue;
                }
                ret = read(fd, rdbuf, sizeof(rdbuf) - 1);
                if (ret < 0) {
                        perror("read()");
                        return 1;
                }
                rdbuf[ret] = '\0';
                printf("interrupt, value is: %s", rdbuf);
        }
        close(fd);

        teardown(gpio_nr);

        return 0;
}

GPIOの設定はsetup()関数の中で行っています。
exportした後に、direction, edge, pullud エントリを設定してfalling edgeの割り込みを待ち受けるように設定します。

割り込み待ちはmain()のループの中で、valueエントリのPOLLPRIイベントをpollすることで実現できます。

gpio-irq-demo.cをコンパイルし、GPIO23番とGNDをスイッチにつないで(こちら参照)から以下のように実行します。

% sudo ./gpio-irq-demo 23
configureing GPIO 23 as
  input, interrupt on falling edge, internal pull-up

(スイッチを押す)
interrupt, value is: 0
interrupt, value is: 0
interrupt, value is: 0

(Ctrl-C)
^Csignal 2 received.
clean up GPIO 23

/dev/memを使わずにGPIOのpullup設定ができるようになりました。

今後

カーネルをいじりだした時は、実装したものをRPiのカーネルかあわよくばmainlineに取り込んでもらおうかと思っていたのですが、
GPIO subsystemとは別に Pin control subsystem というものが実装進行中で、
機能がマルチプレクスされたピンのコントロールやpullup/downのコントロールもそちらに含まれるようなのです。
(参考:http://www.df.lth.se/~triad/papers/pincontrol-gpio-update.pdf
そしてkernel 3.7系からbcm2835のPin controlドライバも登場しています。

GPIOとPin controlは密接に関係していて、両方をきちんと理解した上で実装を進める必要があります。
なのでPin control subsystemを勉強してからまたどうするか改めて考えてみたいと思います。
今回はとりあえずこの辺で。

Raspberry Pi で音声認識

Raspberry Piオープンソースの日本語音声認識ソフトJuliusをコンパイル・実行してみました。

ダウンロード、ビルド

http://julius.sourceforge.jp/ からSource(tarball)をダウンロードします。
執筆時点で julius-4.2.3.tar.gz です。
またディクテーションキット、文法認識キットをダウンロードします。
それぞれ dictation-kit-v4.2.3.tar.gz, grammar-kit-v4.1.tar.gz です。

juliusのコンパイル

% tar xvf julius-4.2.3.tar.gz
% cd julius-4.2.3
% ./configure
% make
% cd julius-simple
% make

ディクテーションキット、文法認識キットを適当な場所に展開

% mkdir -p ~/lib/julius
% cd ~/lib/julius
% tar xvf <dounload dir>/dictation-kit-v4.2.3.tar.gz
% tar xvf <download dir>/grammar-kit-v4.1.tar.gz

マイク

Raspberry Piにはオーディオの入力がないので、USBマイクなどを使います。
以前買ったやつでもいいんですが、今回はとりあえずこれで進めました。

【2009年モデル】ELECOM USBスタンドマイク ブラック (PS3対応) HS-MC02UBK

【2009年モデル】ELECOM USBスタンドマイク ブラック (PS3対応) HS-MC02UBK

(注)USBハブをかませるとどうにもまともな音が録れませんでした。USBオーディオバイスRaspberry PiのUSBポートに直接挿した方がいいです。

このマイクの入力音声はモノラルの44.1kHz固定です。
juliusは16kHzの音声を使用するので44.1kHzを16kHzに変換する必要がありますが、以下の2つの方法を試しました。

1. juliusの -48 オプションで48kHz入力を->16kHzにダウンサンプルする
44.1kHz入力の場合は14.7kHzになってしまいますが、まあまあ大丈夫です。

2. pulseaudioを使ってリサンプルする
resample-method = trivial という設定をするのですが、trivialってどんなん?と思いつつもとりあえず大丈夫そうです。
以下ではこちらの方法を紹介します。

pulseaudioのインストールと設定

% sudo apt-get install pulseaudio

/etc/pulse/daemon.conf に以下の設定をします。

resample-method = trivial

起動(ALSAにアクセスにいけば自動で起動するので、マニュアルで起動する必要はないと思います)

pulseaudio -D

音声入力デバイスが1つしかない場合は必要ないかもしれませんが、一応入力デバイスを確認し、デフォルトのソースを選択します。

% pactl list short sources
0       alsa_output.usb-C-Media_Electronics_Inc._USB_PnP_Sound_Device-00-Device.analog-stereo.monitor   module-alsa-card.c      s16le 2ch 48000Hz       SUSPENDED
1       alsa_input.usb-C-Media_Electronics_Inc._USB_PnP_Sound_Device-00-Device.analog-mono      module-alsa-card.c      s16le 1ch 48000Hz       SUSPENDED
2       alsa_output.platform-bcm2835_AUD0.0.analog-stereo.monitor       module-alsa-card.c      s16le 2ch 48000Hz       SUSPENDED

この場合は1をソースにしたいので、

% pacmd set-default-source 1
Welcome to PulseAudio! Use "help" for usage information.
>>> >>>

とします。

これで音声が録音できるかどうか確認しましょう。alsamixerで入力音量を調整してから、arecordで録音してみます。

% arecord -c 1 -r 16000 -f S16_LE a.wav

a.wavを再生して、音声を確認してみてください。しょぼいデバイスだと電源ノイズとかが乗ってたりしがちですが、
音声認識にはあまり影響しないはずなんで(たぶん)とりあえず気にせず、サンプリングレートがきちんとあってることだけ確認して進めます。

動かしてみる

pulseaudioを使用しない場合は、juliusへの入力デバイスを選択するには環境変数ALSADEVを設定します。
(pulseaudioを使用する場合は設定しないでください)

% export ALSADEV=hw:1
グラマーキット動作例
% julius-simple -C ~/lib/julius/grammar-kit-v4.1/testmic.jconf
STAT: include config: /home/penkoba/lib/julius/grammar-kit-v4.1/testmic.jconf
STAT: include config: /home/penkoba/lib/julius/grammar-kit-v4.1/hmm_ptm.jconf
STAT: jconf successfully finalized
STAT: *** loading AM00 _default
Stat: init_phmm: Reading in HMM definition
 :
<<< please speak >>>

(「みかんよんこをください」と発話)

sentence1: <s> 蜜柑 4 個 を ください </s>
wseq1: 7 0 1 2 3 4 8
phseq1: silB | m i k a N | y o N | k o | o | k u d a s a i | silE

cmscore1: 1.000 0.999 0.993 1.000 1.000 1.000 1.000
score1: -4083.436279
<<< please speak >>>

それなりに認識します。

ディクテーションキット動作例
% julius-simple -C ~/lib/julius/dictation-kit-v4.2.3/fast.jconf
STAT: include config: /home/penkoba/lib/julius/dictation-kit-v4.2.3/fast.jconf
STAT: jconf successfully finalized
STAT: *** loading AM00 _default
Stat: init_phmm: Reading in HMM definition
Stat: read_binhmm: binary format HMM definition
 :
<<< please speak >>>

(「きょうわいいてんきですね」と発話)

sentence1:  今日 は いい 天気 です ね 。 
wseq1: <s> 今日:{キョー/コンニチ}:今日:536 は:ワ:は:66 いい:イイ:いい:38 天気:テンキ:天気:507 です:デス:です:121 ね:ネ:ね:67 。:。:。:8 </s>
phseq1: silB | ky o: | w a | i i | t e N k i | d e s u | n e | sp | silE

cmscore1: 0.864 0.054 0.354 0.223 0.409 0.826 0.269 0.387 1.000
score1: -4337.769531 (AM: -4236.425293  LM: -101.344154)

遅いですけど一応動きました。

まとめ

一応、日本語音声認識が動きました。
ディクテーションは(予想通り)処理能力的にちょっと無理がありますが、グラマーありの用途ならばまあまあ使えそうです。
別のUSBオーディオを使った入力も試してみたところ音声のクオリティはだいぶ良かったので、
きちんと性能を出したい場合はそれなりの音声入力環境を整える必要があります。(って当たり前のこと書いてますが...)

Raspberry Pi にシャットダウンボタンをつける

コンソールを使わずに運用する場合でも自力でシャットダウンができないと何かと不便なので、スイッチでシャットダウンできるようにしました。

参考にしたサイト
http://raspi.tv/2013/how-to-use-interrupts-with-python-on-the-raspberry-pi-and-rpi-gpio

準備

まず、以下のように対話モードでpythonを実行し、RPi.GPIOのバージョンが0.5.1以上であることを確認します。

% sudo python
Python 2.7.3 (default, Jan 13 2013, 11:20:46) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import RPi.GPIO as GPIO
>>> GPIO.VERSION
'0.5.2a'

配線

Raspberry Piにスイッチを接続します。

P1ピンヘッダの 14ピン(GND) と 16ピン(GPIO23) から、スイッチに配線します。

サンプルの動作確認

まずは割り込み動作の確認のため、参考サイトのサンプルをそのまま試してみます。

% wget http://raspi.tv/download/interrupt1.py.gz
gunzip interrupt1.py.gz

interrupt1.py

#!/usr/bin/env python2.7  
# script by Alex Eames http://RasPi.tv/  
# http://raspi.tv/2013/how-to-use-interrupts-with-python-on-the-raspberry-pi-and-rpi-gpio  
import RPi.GPIO as GPIO  
GPIO.setmode(GPIO.BCM)  

# GPIO 23 set up as input. It is pulled up to stop false signals  
GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)  

print "Make sure you have a button connected so that when pressed"  
print "it will connect GPIO port 23 (pin 16) to GND (pin 6)\n"   
raw_input("Press Enter when ready\n>")  

print "Waiting for falling edge on port 23"  
# now the program will do nothing until the signal on port 23   
# starts to fall towards zero. This is why we used the pullup  
# to keep the signal high and prevent a false interrupt  

print "During this waiting time, your computer is not"    
print "wasting resources by polling for a button press.\n"  
print "Press your button when ready to initiate a falling edge interrupt."  
try:  
        GPIO.wait_for_edge(23, GPIO.FALLING)  
        print "\nFalling edge detected. Now your program can continue with"   
        print "whatever was waiting for a button press."  
except KeyboardInterrupt:  
        GPIO.cleanup()  # clean up GPIO on CTRL+C exit  
GPIO.cleanup()          # clean up GPIO on normal exit 

このスクリプトを、rootで起動します。
(pin16とpin6をボタンに繋げと書いてありますが、pin6の代わりにpin14を使用しました。
 14と16が隣同士なのでこちらの方がわかりやすいかと)

% sudo ./interrupt1.py
Make sure you have a button connected so that when pressed
it will connect GPIO port 23 (pin 16) to GND (pin 6)

Press Enter when ready
>
Waiting for falling edge on port 23
During this waiting time, your computer is not
wasting resources by polling for a button press.

Press your button when ready to initiate a falling edge interrupt.

(ここでスイッチを押す)

Falling edge detected. Now your program can continue with
whatever was waiting for a button press.

スイッチに反応してコマンドが終了します。

スクリプト改造

上記のサンプルを元に、スイッチが押されたらシャットダウンを行うコマンドを作ります。
/usr/local/sbin/shutdown-btn.py

#!/usr/bin/env python2.7  
import RPi.GPIO as GPIO  
import os
GPIO.setmode(GPIO.BCM)  

GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)  

try:  
        GPIO.wait_for_edge(23, GPIO.FALLING)  
except KeyboardInterrupt:  
        GPIO.cleanup()  # clean up GPIO on CTRL+C exit  
GPIO.cleanup()          # clean up GPIO on normal exit  
os.system("/sbin/shutdown -h now")

これをrootで起動し、スイッチを押せばシャットダウンが行われます

sudo /usr/local/sbin/shutdown-btn.py

(スイッチを押す)

Broadcast message from root@pi2 (pts/1) (Thu Sep 26 01:27:26 2013):

The system is going down for system halt NOW!

init script作成

自動で起動したいので、サービス化します。
下記内容で、/etc/init.d/shutdown-button を作成します。

### BEGIN INIT INFO
# Provides: shutdown-button
# Required-Start:
# Required-Stop:
# Default-Start: 1 2 3 4 5 6
# Default-Stop: 0
# Short-Description: Shutdown Button
# Description: wait Shutdown Button
### END INIT INFO
#! /bin/sh
# /etc/init.d/shutdown-button
PIDFILE=/var/run/shutdown-btn.pid
case "$1" in
        start)
                if [ -f $PIDFILE ]; then
                        echo $PIDFILE exists.
                        exit 1
                fi
                start-stop-daemon -S -x /usr/local/sbin/shutdown-btn.py -b -m -p $PIDFILE
                ;;
        stop)
                if [ ! -f $PIDFILE ]; then
                        echo $PIDFILE not found.
                        exit 1
                fi
                start-stop-daemon -K -p $PIDFILE
                rm $PIDFILE
                ;;
        *)
                echo "Usage: /etc/init.d/shutdown-button {start|stop}"
                exit 1
                ;;
esac
exit 0

update-rc.dでinit scriptを登録します。デフォルトのrunlevel設定と違うと警告が出ますが、これでOKです。

% sudo update-rc.d shutdown-button defaults
update-rc.d: using dependency based boot sequencing
update-rc.d: warning: default start runlevel arguments (2 3 4 5) do not match shutdown-button Default-Start values (1 2 3 4 5 6)
update-rc.d: warning: default stop runlevel arguments (0 1 6) do not match shutdown-button Default-Stop values (0)

これで /usr/local/sbin/shutdown-btn.py が自動で起動されるようになりました。

まとめ

とりあえずシャットダウンスイッチが動作するようになりました。
が、これだけのために専用のプロセスがずっと待ち受けているのはちょっと不格好な気がしますし、pythonのモジュールを使ってちょいちょいとスクリプトを書いただけで中身がよくわかりません。
GPIOがらみはもう少し掘り下げて、応用がきく状態にしたいと思っています。