国内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
スクリプト
上記方針を実現するため、 /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になにかいることがわかります。
問題点
「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)に提供するサービス
内部(宅内)に提供するサービス
サービスの有効化・起動
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>
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/06/05
- メディア: Personal Computers
- 購入: 4人 クリック: 29回
- この商品を含むブログ (3件) を見る
このマイクの入力音声はモノラルの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)
遅いですけど一応動きました。
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'
サンプルの動作確認
まずは割り込み動作の確認のため、参考サイトのサンプルをそのまま試してみます。
% 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 が自動で起動されるようになりました。