Pythonプログラムをデーモン起動する(Raspbian)

Pythonで実装したプログラムを、Raspberry Pi(Raspbian)でデーモン起動させるときのメモ。

python-daemonのインストール

python-daemonは、Pythonプログラムをdaemon化できるパッケージである。

sudo apt-get install python-daemon

デーモンプログラム

Pythonプログラムはこんな感じ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import serial
import time

from daemon import DaemonContext
from daemon.pidlockfile import PIDLockFile

if __name__ == '__main__':
        # 以下がデーモンプロセスとして実行される
        # pidfileが/tmpにないと、動作しないらしい
        with DaemonContext(pidfile=PIDLockFile('/tmp/serial-arduino.pid')):
                main()

起動スクリプト

サービス起動させるための起動デーモンは以下の通り。

$ sudo vi /etc/init.d/hoged

#!/bin/sh

### BEGIN INIT INFO
# Provides:        hoged
# Required-Start:  $local_fs $remote_fs $syslog
# Required-Stop:   $remote_fs
# Default-Start:   2 3 4 5
# Default-Stop: 
# Short-Description: Start hoged daemon
### END INIT INFO

PATH=/sbin:/bin:/usr/sbin:/usr/bin
# Source function library.
. /lib/lsb/init-functions

# Path to the script
DAEMON=/usr/local/sbin/python-hoge.py
PROG=serial-arduino
PIDFILE=/tmp/hoged.pid
LOCKFILE=/tmp/hoged.pid.lock

test -f $DAEMON || exit 0

lock_hoged() {
        if [ -x /usr/bin/lockfile-create ]; then
                lockfile-create $LOCKFILE
                lockfile-touch $LOCKFILE &
                LOCKTOUCHPID="$!"
        fi
}

unlock_hoged() {
        if [ -x /usr/bin/lockfile-create ] ; then
                kill $LOCKTOUCHPID
                lockfile-remove $LOCKFILE
        fi
}

case "$1" in
    start)
        log_daemon_msg "Starting $PROG server" "$PROG"
        lock_hoged
        start-stop-daemon --start --quiet --oknodo --pidfile $PIDFILE --startas $DAEMON -- -p $PIDFILE
        status=$?
        unlock_hoged
        log_end_msg $status
        ;;
    stop)
        log_daemon_msg "Stopping $PROG server" "$PROG"
        start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE
        log_end_msg $?
        rm -f $PIDFILE
        ;;
    status)
        status_of_proc -p $PIDFILE $DAEMON $PROG
        ;;
    restart)
        stop
            start
        ;;
    *)
        echo "Usage: $PROG {start|stop|status|restart}"
        exit 1
        ;;
esac

最後に変更を反映。

sudo update-rc.d hoged defaults

Raspberry Piをシングルユーザモードで起動する

ユーザ, パスワードを忘れてしまって、パスワードを設定しなおしたい場合や、
起動時の設定を誤ってしまった場合など、シングルユーザモードでRaspberry Piにログインする方法

cmdline.txtをエディタで開き、赤字の部分を追加します。

dwc_otg.lpm_enable=0 init=/bin/sh console=ttyAMA0,115200 \
kgdboc=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait logo.nologo

cmdline.txtに、init=/bin/shを追加した上で、
Raspberry Piを起動すると、シングルユーザモードで起動するとのこと。

ただし起動直後は、/以下がreadonlyの状態でマウントされているので、
ファイルの読み込みはできても、変更などを行うことができない。

mount -o remount,rw /

で再マウントを行うことで、読み書きが可能となる。

Raspberry Pi – Arduino間でシリアル通信を行う

シリアル通信を行うことが可能なscreenコマンドと、
Pythonでシリアル通信を行うことが可能なpython-serialモジュールを
apt-getを利用してRaspberry Piにインストールする

sudo apt-get install screen
sudo apt-get install python-serial

次に、Pythonでシリアル通信スクリプトを作成する

vi serial-arduino.py

このときにファイル名をserial.pyとかいう名前にしてしまうと、
Pythonのserialモジュールと名前が重複してしまい、

AttributeError: ‘module’ object has no attribute ‘Serial’

というエラーがでてしまうので、他の名前にするのが良い。

USBでRaspberry PiとArduinoを接続し、
なんというデバイス名称で認識されているか確認する

dmesg
lsusb

シリアル通信でArduinoから定期的に送られてくるデータをもとに、
JSONファイルを作成するサンプルは以下の通り。

  • カンマ区切りデータの場合
    import serial
    
    def main():
            # デバイス名: /dev/ttyACM0 通信速度: 9600bps
            con=serial.Serial('/dev/ttyACM0', 9600)
            # デバイス名を出力する
            print con.portstr
            while 1:
                    # 1行ごとにデータを取得する
                    str=con.readline()
                    # ファイルに書き込み
                    f = open('/tmp/data.json', 'w')
                    f.write("[{\"data_1\":" + str + "}]")
                    f.close()
    
    if __name__ == '__main__':
            main()
    

  • JSON形式の場合

    import serial
    import json
    
    def main():
            # デバイス名: /dev/ttyACM0 通信速度: 9600bps
            con=serial.Serial('/dev/ttyACM0', 9600)
            # デバイス名を出力する
            print con.portstr
            while 1:
                    try:
                            # strという変数名は使用できない
                            strs=con.readline()
                            dec=json.loads(strs)
    
                            f = open('/tmp/data.json', 'w')
                            f.write(strs)
                            f.close()
                    except KeyError:
                            print "KeyError"
                    except ValueError:
                            print "ValueError"
                    except NameError:
                            print "NameError"
    
    if __name__ == '__main__':
                    main() 
    

実行してJSONが吐き出されるか確認する

python serial-arduino.py

Raspberry Piでスクリーンショットを撮影する

Raspberianでスクリーンショットを撮るときは、Scrotがオススメ。

Scrotをインストール

$ sudo apt-get install scrot

ウインドウを指定してスクリーンショットを撮るときは、
sオプションを付けて実行し、該当のウインドウをクリックする

$ scrot -s

以上でスクリーンショットを撮影することができる

Raspberry Piのインストール

Raspberry Piの起動と終了

Raspberry Piの起動

Raspberry Piは、電源を投入すると自動的に起動する
インストール時のログイン名/パスワードは、
以下の通り設定されていて、piユーザにはsudo権限が付与されている
+ user: pi
+ pass: raspberry
Guiを開始する場合は、以下のコマンドを実行する

$ startx

Guiの終了は、メニューからログアウトを選択する

Raspberry Piの終了

終了や再起動は、通常のLinuxと同じで、
以下のいずれかのコマンドを実行する

$ sudo reboot
$ sudo shutdown -h now

Rasbperianのセットアップ

Raspberianのインストール

まず、Raspberry Piで使用するSDカードを
SD Formatterを利用して初期化する
+ https://www.sdcard.org/downloads/formatter_4/
対象のSDカードを選択して、初期化を実行する
SDカードのフォーマット
初期化されたSDカードにNOOBSをコピーする
NOOBSのコピー

Raspberry Pi + Motion + WebCamで動体検知を行う

Raspberry Piに接続したWebCamから映像を取得可能なパッケージはいくつか有名なものが存在するようだが、今回はMotionという動体検知を行うことのできるパッケージを用いて、WebCamの映像を取得してみる。

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

apt-getから取得することができる

sudo apt-get install motion

設定

今回はMotionをデーモン起動させて常時稼働させる。設定ファイルは以下のように変更した。

# Image width
width 640
# Image height
height 480
# Maximum number of frames to be captured per second.
# フレーム数を大きくしすぎるとRaspberry Piで処理しきれなくなる
framerate 3
# Output 'normal' pictures when motion is detected
# 動体検知時に画像を出力しない
output_pictures off
# Use ffmpeg to encode movies in realtime
# 動体検知時に動画を出力しない
ffmpeg_output_movies off
# Make automated snapshot every N seconds
snapshot_interval 1
# Locate and draw a box around the moving object.
# 動体マーカ
locate_motion_mode on
# Target base directory for pictures and films
target_dir /var/www/html
# File path for snapshots (jpeg or ppm) relative to target_dir
snapshot_filename snapshot
# Restrict stream connections to localhost only
# ローカルホスト以外からのアクセスを許可する
stream_localhost off
# Restrict control connections to localhost only
# ローカルホスト以外からのアクセスを許可する
webcontrol_localhost off

サービスの開始

USBカメラを繋ぎ、認識されていることを確認する。

lsusb

Bus 001 Device 015: ID 0c45:62e0 Microdia MSI Starcam Racer

設定ができたらサービスを開始する。でも正常にサービス開始しない。

sudo service motion start
Apr 20 18:42:55 raspberrypi motion[1410]: Not starting motion daemon, disabled via /etc/default/motion ... (warning).

/etc/default/motionも変更しないといけないらしい。

sudo vi /etc/default/motion

# set to 'yes' to enable the motion daemon
start_motion_daemon=yes

再度、サービス開始。
今度はちゃんと開始された。

sudo service motion start

以下のエラーが発生する場合は、画像の出力先のパーミッションに問題ある場合があるので、パーミッションを変更する。

Apr 20 19:28:15 raspberrypi motion[7929]: [1] [ERR] [ALL] put_picture: Can't write picture to file /var/www/html/snapshot.jpg - check access rights to target directory
                                          Thread is going to finish due to this fatal error:
Apr 20 19:28:15 raspberrypi motion[7929]: [1] [ERR] [EVT] event_image_snapshot: Could not create symbolic link [snapshot.jpg]:

Raspberianをインストール後にすること

Raspberry PiにRaspberianをインストール後、まずすること。普段からDebianを使っている人は違和感ないのかもしれないけど、RHEL系で生きているとDebian系の使い勝手が分からなくて苦しい。

ロケールの変更

ロケールを日本語に設定すると文字化けでうまく表示されないので、en_US.UTF-8.UTF-8のままでよいかも。

$ sudo dpkg-reconfigure locales

タイムゾーンの変更

タイムゾーンを東京に変更する。

$ sudo cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
$ sudo vi /etc/timezone
Asia/Tokyo

キーボードの設定

日本語キーボードに対応する。

$ sudo vi /etc/default.keyboard

# KEYBOARD CONFIGURATION FILE

# Consult the keyboard(5) manual page.

XKBMODEL="jp106"
XKBLAYOUT="jp"
XKBVARIANT=""
XKBOPTIONS=""

BACKSPACE="guess"

ランレベルの設定

デフォルトだとXWindowが立ち上がるが、これをランレベル3に変更する。

sudo systemctl set-default multi-user.target

固定IPアドレスを設定

Raspbrianのバージョンによって設定方法が異なる。

Raspbrian jessie

固定IPの設定は、/etc/network/interfacesではなく、/etc/dhcpcd.confに追記する。

$ sudo vi /etc/dhcpcd.conf

interface eth0
static ip_address=192.168.0.10/24
static routers=192.168.0.1
static domain_name_servers=192.168.0.1

以下のコマンドをいろいろ試したけど、新しい設定が全く反映されない。

$ sudo service dhcpcd restart
$ sudo service dhcpcd reload
$ sudo service dhcpcd force-reload
$ sudo service networking restart
$ sudo service ifup@eth0 restart
$ sudo systemctl restart dhcpcd
$ sudo systemctl restart networking
$ sudo systemctl restart ifup@eth0

仕方ないので端末再起動。

$ sudo reboot

Raspbrian wheezy

固定IPの設定は、/etc/network/interfacesに追記する。

$ sudo vi /etc/network/intefeces

インストール直後には、DHCP設定となっているので、固定アドレスとする場合は以下のように変更する。

auto lo

iface lo inet loopback
iface eth0 inet static

address 192.168.0.2
netmask 255.255.255.0
gateway 192.168.0.1

allow-hotplug wlan0
iface wlan0 inet manual
wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
iface default inet dhcp

無線LANの設定

Raspberry Piに無線LAN接続する場合は、以下の手順で行う。今回は、PLANEXのGW-USNANO2Aを使用した。

  1. Raspberry PiのUSBポートに挿入
  2. wpa_passphraseコマンドを、/etc/wpa_supplicant/wpa_supplicant.confに出力する
  3. /etc/wpa_supplicant/wpa_supplicant.confに、認証方式などの情報を追記する

WPA2-PSK(AES)の場合は、以下の通りとなる。

$ sudo wpa_passphrase SSID PASS_PHRASE >> /etc/wpa_supplicant/wpa_supplicant.conf
$ sudo vi /etc/wpa_supplicant/wpa_supplicant.conf

country=GB
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
        ssid="SSID"
        #psk="PASS_PHRASE"
        psk=xxxxxxxxxxxxxxxxxxxxxxxxxxx
        proto=RSN
        key_mgmt=WPA-PSK
        pairwise=CCMP
        group=CCMP
}

無線LANインタフェースの状況を確認するためには、

iwconfig wlan0

識別できているSSIDの一覧を取得するためには、

iwlist wlan0 scan | grep SSID

                    ESSID:"guest"
                    ESSID:"test"
                    ESSID:""
                    ESSID:"free"
                    ESSID:""

以下のファイルも変更する。
wlan0インタフェースを自動起動設定とし、wlan1インタフェースの無効化している。

$ sudo vi /etc/network/interfaces

# interfaces(5) file used by ifup(8) and ifdown(8)

Please note that this file is written to be used with dhcpcd
For static IP, consult /etc/dhcpcd.conf and 'man dhcpcd.conf'

# Include files from /etc/network/interfaces.d:
source-directory /etc/network/interfaces.d

auto lo
iface lo inet loopback

iface eth0 inet manual

auto wlan0
allow-hotplug wlan0
iface wlan0 inet manual
     wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
 
#allow-hotplug wlan1
#iface wlan1 inet manual
#    wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf

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

必要なパッケージをインストールする。

$ sudo apt-get install chkconfig
$ sudo apt-get install screen
$ sudo apt-get install apache2
$ sudo chkconfig ntp off
$ sudo chkconfig apache2 on

パッケージの更新もしておく。

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo rpi-update

プロキシ環境でapt-getを実行する場合は、以下のファイルを変更する。

$ sudo vi /etc/apt/apt.conf.d/10proxy

追記内容は以下の通り。

Acquire::http::Proxy "http://192.168.x.x:8080";

Vimの起動モードを変更

Viで矢印キーを押したときにABCDと出てしまう問題は、VimをVi互換モードではなく通常モードで起動することで直る。

~/.vimrcに設定を保存できる。sudoでvimを使うときのために/root/.vimrcにも追記しておく。

$ vi ~/.vimrc

:set nocompatible
:set backspace=indent,eol,start
:set number

~/.vimrcに書ける便利な設定がいろいろあるみたい。

設定 内容
:set nocompatible 通常モード起動
:set backspace=indent,eol,start バックスペースキーの挙動を指定
:set number 行番号を表示

もしくは、Vimのインストールでも直る。

$ sudo apt-get install vim

Raspberry piがRead-Only Filesystemになってしまったときの対処法

Raspberry piの設定ファイルを書き換えても再起動すると、元の内容に戻ってしまい変更が反映されない。

pi@raspberrypi ~ $ dmesg | egrep "readonly|read-only"
[    2.386594] mmc0: host does not support reading read-only switch, assuming write-enable
[    2.526799] EXT4-fs (mmcblk0p6): INFO: recovery required on readonly filesystem
[    3.242388] EXT4-fs (mmcblk0p6): orphan cleanup on readonly fs
[    3.370433] VFS: Mounted root (ext4 filesystem) readonly on device 179:6.

dmesgで見ると、ファイルシステム全体がReadonlyになっていて、これがファイルがうまく保存されない原因のようだ。起動中にSDカードを抜き出したり、電源を落としたときにファイルシステムが壊れたか。

recovery required on readonly filesystem

というわけでさっそく修復してみる。fsckコマンドで修復してみるも、既にマウントされているのでコマンド実行できないと怒られる。とりあえず早く修復して別の作業に取り掛かりたいので、カーネル起動オプション設定ファイル「cmdline.txt」に、fsckを毎回起動時に実行する設定を追記し、Raspberry Piを再起動。

fsck.mode=force

fsckが走ってとりあえず修復。よかった。