sstea備忘録

日々のPCライフのメモ、備忘録、及びそれに類推する何か。
記載内容に間違い等を発見された場合はコッソリ教えてください... → sstea<a>aol.jp (<a> を @ に変えてください)


joystick

続、Raspberry Pi をジョイスティックで操作する


前回からの続きになります。
前回、Raspberry Piでジョイスティックの入力検出する jsinputd デーモンを作りました。
今回は jsinputd デーモンを使用し、ジョイスティックによりRaspberry Piに実際にアクションさせてみたいと思います。

jsinputd デーモン、及びスクリプトは全て /home/pi/local/bin/ に置いてあるという前提で、下記は記述しています。新しく作成するスクリプトも同ディレクトリに置きます。

◆ジョイスティックのキー入力判定とアクション
ジョイスティックのどのボタンが押された時に、どういうアクションを行なうかを書いたファイルが下記です。

jscmd.sh
#!/bin/bash

JSFILE=$1

update_input() {
    js=(`cat ${JSFILE}`)
    bt_left=${js[0]}
    bt_down=${js[1]}
    bt_right=${js[2]}
    bt_up=${js[3]}
    bt_L1=${js[4]}
    bt_R1=${js[5]}
    bt_L2=${js[6]}
    bt_R2=${js[7]}
    bt_select=${js[8]}
    bt_start=${js[9]}
    bt_L3=${js[10]}
    bt_R3=${js[11]}
    st_LH=${js[13]}
    st_LV=${js[14]}
    st_RH=${js[15]}
    st_RV=${js[16]}
    st_crossH=${js[17]}
    st_crossV=${js[18]}
}

joystick_command() {
    if [ ${bt_start} -eq 1 ] && [ ${bt_select} -eq 1 ] &&
       [ ${bt_L1} -eq 1 ] && [ ${bt_R1} -eq 1 ] &&
       [ ${bt_L2} -eq 1 ] && [ ${bt_R2} -eq 1 ]; then
        /home/pi/local/bin/talk.sh "Shutdownします"
        sudo shutdown -h now
        exit 0
    fi

    if [ ${bt_start} -eq 1 ]; then
        /home/pi/local/bin/jihou.sh &
    fi
}

update_input
joystick_command

ソフトで1つ目に検出するボタンがジョイスティックの実際のどのボタンに対応するか(startボタンなのか、L1ボタンなのか)は、使用するジョイスティック毎に異なりますので、まずは update_input にてそれぞれ対応する変数に入れています。

joystick_command の部分が実際のアクション定義部分です。

上記例では、start+select+L1+L2+R1+R2 の同時押しで、Raspberry Piをシャットダウンします。
またその旨を音声出力しています。
もう一つのコマンドはstart ボタンだけを押した場合で、その時の時間をお知らせします。
talk.sh や jihou.sh は前々回に作成したスクリプトです。

この部分の追加、カスタマイズはお好きにどうぞ。
(拡張版を↓おまけ2に用意しました。)

# 最初 "#!/bin/sh" としていたのですが、
# "js=(`cat ${JSFILE}`)" という書き方が、"#!/bin/sh" だと駄目のようでした。
# とりあえずですが、#!/bin/bash 固定としておきます。

jsmonitor.sh
#!/bin/sh

JSDEV=$1
JSFILE=$2

while [ `cut -f1 -d. /proc/uptime` -lt 20 ]; do
    sleep 1
done

/home/pi/local/bin/jsinputd ${JSDEV} ${JSFILE} &

sleep 0.5

while inotifywait -qq -e modify ${JSFILE}
do
    /home/pi/local/bin/jscmd.sh ${JSFILE}
done

ジョイスティック入力検出のための jsinputd デーモンをバックグラウンド起動し、デーモンの出力ファイルに変更があった時(=キー入力に変更があった時)だけ jscmd.sh が呼ばれます。
キー入力に変化がない場合(無入力状態、またはキーを押しっぱなしの場合)は inotifywait により待ち状態となります。

jsinputd の実行の後に 0.5 秒の sleep を入れているのは、デーモンが動作し出力ファイルが作成されるのを待つためです。
一番最初の while の中の sleep は気にしないでください。(↓◆おまけ1で補足します。)

上記スクリプトを取ってきて、実行権限を与えます。
$ cd /home/pi/local/bin/

$ curl -O http://sstea.blog.jp/raspi/script/jscmd.sh
$ curl -O http://sstea.blog.jp/raspi/script/jsmonitor.sh

$ chmod 755 jscmd.sh jsmonitor.sh
実行してみます。
$ ./jsmonitor.sh /dev/input/js0 /tmp/jsinput
意図通りに動くか確認してみましょう。
jscmd.sh で jihou.sh をバックグラウンド実行させているので、start ボタンを連打しても、ちゃんと押した回数、時間報告してくれます。、

# 上記 jsmonitor.sh は Ctrl-C で終了しても、バックグラウンドで動いている jsinputd は終了しません。
# "killall jsinputd" してあげてください。
# もしくはUSBアダプタを抜いてやると終了します。

◆jsmonitor.shの自動起動
毎回、自分で jsmonitor.sh を実行するのは面倒なので自動的に起動させたいですが、起動時に自動実行させても起動時点でUSBアダプタが刺さっていない(起動後に挿す場合とか)と、どのみち自分で実行する必要があります。

幸いにも Linux には udev というデバイスのプラグアンドプレイに合わせて処理を行なわさせる機構が備わっています。(正直、udev はあまり詳しくないですが。。。)

jsevent.sh
#!/bin/sh

JSFILE=/tmp/jsinput

if [ $1 = "add" ]; then
    /home/pi/local/bin/jsmonitor.sh ${DEVNAME} ${JSFILE} &
elif [ $1 = "remove" ]; then
    rm -f ${JSFILE}
fi
ジョイスティックの抜き差し時に、上記 jsevent.sh を実行する udev のルールを追加してみましょう。

/etc/udev/rules.d/99-joystick.rules
SUBSYSTEMS=="input", KERNEL=="js[0-9]", ACTION=="add", RUN="/home/pi/local/bin/jsevent.sh add"
SUBSYSTEMS=="input", KERNEL=="js[0-9]", ACTION=="remove", RUN="/home/pi/local/bin/jsevent.sh remove"

上記ルールを下記コマンドにより追加します。
$ curl -O http://sstea.blog.jp/raspi/script/jsevent.sh
$ chmod 755 jsevent.sh

$ curl -O http://sstea.blog.jp/raspi/script/99-joystick.rules
$ sudo mv 99-joystick.rules /etc/udev/rules.d/99-joystick.rules
udev をリスタートします。
$ sudo service udev restart
これで完成です。
USBアダプタのプラグアンドプレイに合わせて、jsmonitor.sh を実行できるようになりました。

# ちなみに jsevent.sh の DEVNAME ですが、環境変数として設定されるようです。
# おそらく "udevadm info --query=property --name=/dev/input/js0" で
# 表示されるパラメータが使用可能だと思います。

◆おまけ1
udev ルールでプラグアンドプレイに合わせてスクリプトを実行するようにしたわけですが、プラグアンドプレイ時は問題なかったのですが、当初は起動時からUSBアダプタが刺さっている場合に問題がありました。

jsinputd が /tmp/jsinput ファイルを作成するのですが、起動時点で既にUSBアダプタが刺さっている場合、なぜかファイル作成に失敗していたのです。

いろいろデバッグした結果、推測も混じりますが、以下の事が分かりました。
起動時に既に条件を満たしたudevルールがある場合、他の初期化が完了する前の結構早い段階で実行されるようです。(fstab のマウント等より先のようです。)
そのため、まだファイルシステムがread onlyの状態(remount前)に書き込みをしようとして失敗しているようでした。(remountが終わっても、/tmp を ramdisk 化しているため、tmpfs のマウント前の /tmp に書き出していた場合もありました。)

単純に sleep 10 とかを入れて、初期化完了待ちをしようかと思いましたが、そうするとプラグアンドプレイ時に、それだけ待たされてしまいます。
苦肉の策としてですが、起動時からの時間が20秒経っていなければ、20秒経つまで待ちにするという sleep を入れました。
その結果、起動時からUSBアダプタが刺さっている場合でも、プラグアンドプレイ時でも期待した動作をするようになりました。

◆おまけ2
上記の jscmd.sh ではボタンの押しっぱなしによる連続入力が検出できません。
それを検出できるようにした拡張版が↓です。
また mpc 関連の操作ができるようにコマンドを追加しました。

jscmd2.sh
#!/bin/bash

JSFILE=$1

update_input() {
    :  (省略)
}

joystick_command() {
    if [ ${bt_start} -eq 1 ] && [ ${bt_select} -eq 1 ] &&
       [ ${bt_L1} -eq 1 ] && [ ${bt_R1} -eq 1 ] &&
       [ ${bt_L2} -eq 1 ] && [ ${bt_R2} -eq 1 ]; then
        /home/pi/local/bin/talk.sh "Shutdownします"
        sudo shutdown -h now
        exit 0
    fi
    if [ ${bt_L3} -eq 1 ] && [ ${bt_R3} -eq 1 ] &&
       [ ${bt_L1} -eq 1 ] && [ ${bt_R1} -eq 1 ] &&
       [ ${bt_L2} -eq 1 ] && [ ${bt_R2} -eq 1 ]; then
        /home/pi/local/bin/talk.sh "Rebootします"
        sudo reboot
        exit 0
    fi


    if [ ${bt_start} -eq 1 ]; then
        /home/pi/local/bin/jihou.sh &
        WAIT=5
        return 1

    fi

    if [ ${bt_right} -eq 1 ]; then
        mpc toggle
        exit 0
    fi
    if [ ${bt_down} -eq 1 ]; then
        mpc stop
        exit 0
    fi

    if [ ${bt_L1} -eq 1 ]; then
        mpc prev
        WAIT=5
        return 1
    fi
    if [ ${bt_R1} -eq 1 ]; then
        mpc next
        WAIT=5
        return 1
    fi

    if [ ${st_LH} -ne 0 ]; then
        if [ ${st_LH} -lt 0 ]; then
            mpc seek -1
        else
            mpc seek +1
        fi
        return 1
    fi
    if [ ${st_crossV} -ne 0 ]; then
        if [ ${st_crossV} -lt 0 ]; then
            mpc volume +10
        else
            mpc volume -10
        fi
        return 1
    fi

    return 0

}

while [ 1 ]; do
    input=`cat ${JSFILE}`
    if [ "${input}" != "${prev_input}" ]; then
        prev_input=${input}
        WAIT=0
    fi
    if [ ${WAIT} -ne 0 ]; then
        WAIT=$((${WAIT} - 1))
        sleep 0.1
        continue
    fi


    update_input
    joystick_command
    if [ $? -eq 0 ]; then
        break
    fi
done

update_input に関しては変更ありませんので、元スクリプトを参照。
$ cd /home/pi/local/bin/

$ curl -O http://sstea.blog.jp/raspi/script/jscmd2.sh
$ mv jscmd2.sh jscmd.sh

$ chmod 755 jscmd.sh

Raspberry Pi をジョイスティックで操作する

前回、Raspberry Pi で音を鳴らしてみましたが、今度はインターフェースの改善を試みたいと思います。

Raspberry Pi を操作するのにこれまでは TeraTerm でSSH接続して操作してきましたが、ホストマシンを立ち上げる必要もあり、Raspberry Pi の電源を落とすのにも一苦労(という程の苦労でもないのですが)します。

かと言って、Raspberry Pi に電源スイッチを(物理的に)増設するほどでもない。
何よりソフトソフトしていたいのです。(半田ゴテとかとは無縁の人なので。自分は。)

◆Raspberry Pi にジョイスティックを接続
一昔前に、PS3 の純正ジョイスティックのボタンがヘタってきたために買ったジョイスティック(GAMETECH ワイヤレスバトルパッドターボ3)がありましたので、それでどうにかしたいと思います。

# ジョイスティック?ジョイパッド?ゲームパッド?コントローラ?
# ちなみにアレは何て呼ぶのが正解なんでしょうか?
# とりあえずここではジョイスティックで統一しておきます。

ワイヤレスバトルパッドターボ3 はUSBの無線アダプタと、コントローラとに分かれています。
現在のうちの Raspberry Pi は↓のような構成になっています。緑色に光ってるのが無線アダプタです。

ss_rpi_connect01

このジョイスティックの無線アダプタであるUSBを接続したところ、
/var/log/messages に下記のようなログが出てました。
usb 1-1.2: new full-speed USB device number 10 using dwc_otg
usb 1-1.2: New USB device found, idVendor=1915, idProduct=001e
usb 1-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
usb 1-1.2: Product: PS3 wireless GAME PAD
usb 1-1.2: Manufacturer: ACRUX
input: ACRUX PS3 wireless GAME PAD as /devices/platform/bcm2708_usb/usb1/1-1/1-1.2/1-1.2:1.0/0003:1915:001E.0006/input/input5
hid-generic 0003:1915:001E.0006: input,hidraw0: USB HID v1.11 Gamepad [ACRUX PS3 wireless GAME PAD] on usb-bcm2708_usb-1.2/input0
また、/dev/input/js0 が増えました。
ここまでは何もしていないですが、正常に認識できているようです。

◆理想のジョイスティックキー入力検出を求めて...
さて、これだけだと、ジョイスティックのボタンをポチポチ押しても、無反応です。
少し Google 先生と相談です。。。

pygame という python のモジュールでジョイスティックの入力が取得可能らしいですね。。。
python... 馴染みがないし、分かりませーん。C とシェルくらいしかまともには使えませーん。

その他、いくつか試してみましたが、、、
キー入力をポーリングで検出するのはどうにかしたいですねー。

sleep しながらループしないと top コマンドで CPU 使用率が100%です。
→ sleep 時間が長いと CPU 使用率は下がるが、キー入力を取りこぼす。
     キー入力の反応が悪く、キモチ悪い。
→ sleep 時間が短いと、キー入力の反応はいいものの、CPU 使用率が上がる。
     0.1秒 sleep だとこの処理だけで、CPU 使用率が 10~20% いってしまいます。
     また、反応がいいとはいいつつも、0.1秒 sleep でも少し違和感を感じました。

Raspberry Pi はただでさえ貧相なCPUなので、キー入力がない時は素直に寝ていて欲しいです。
(待ち受け(idle) 時は1%だってCPUを使って欲しくないのです。)

◆Raspberry Pi でのジョイスティックのキー入力検出
自分の求める要件としては、下記4点です。
・ボタンを押した時の動作はシェルで好きに書く事ができる(カスタマイズが可能)。
・キー同時押しが検出可能。(L1+L2+R1+R2+Start+Select でシャットダウンしたいからです。)
・ポーリングでのキー入力検出ではなくて、イベントドリブン(キー入力がある時だけ起きてくる)のがいい。
・キー入力の反応がサクサク。違和感を感じない。

もう少し頑張って探そうかとも思いましたが、どうせ頑張るなら自分で作る方向のベクトルで頑張った方がいろいろ近道かと思い、いろいろ参考にしつつ C で作ってみる事にしました。

自作したジョイスティック入力検出デーモン(jsinputd と命名)が下記です。(結構、真面目に書きました。)
$ curl -O http://sstea.blog.jp/raspi/csrc/jsinputd.c
下記で Raspberry Pi 上でもビルドできると思います。-DDEBUG はなくても可。
(もちろんホスト側のクロスコンパイラでクロスビルドでも全然問題ありません。)
$ gcc jsinputd.c -o jsinputd -O2 -DDEBUG

jsinputd.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/joystick.h>

#ifdef DEBUG
#define ERROR(...)    (fprintf(stderr, "<JSINPUTD:E> " __VA_ARGS__), -1)
#else /* !DEBUG */
#define ERROR(...)    (-1)
#endif /* !DEBUG */

struct joystick_struct {
    int fd;
    unsigned char nr_buttons;
    unsigned char nr_sticks;
    short *button;    /* button[nr_buttons] */
    short *stick;    /* stick[nr_sticks] */
    char *input;    /* input[nr_btn * 2 + nr_stk * 7 + 1] */
};

static int joystick_output(struct joystick_struct *js, const char *out_file);
static int joystick_get_input(struct joystick_struct *js);
static int joystick_get_info(struct joystick_struct *js);
static int joystick_init(struct joystick_struct *js, const char *dev_file);
static void joystick_exit(struct joystick_struct *js);
static void joystick_main(const char *dev_file, const char *out_file);

static int joystick_output(struct joystick_struct *js, const char *out_file)
{
    FILE *fp = stdout;
    unsigned char i, nr_btn, nr_stk;

    if (out_file != NULL) {
        fp = fopen(out_file, "w");
        if (fp == NULL) {
            return ERROR("fopen(%s)\n", out_file);
        }
    }

    nr_btn = js->nr_buttons;
    nr_stk = js->nr_sticks;

    for (i = 0; i < nr_btn; i++) {
        sprintf(&js->input[i * 2], "%2d", js->button[i] ? 1 : 0);
    }
    for (i = 0; i < nr_stk; i++) {
        sprintf(&js->input[nr_btn * 2 + i * 7], "%7d", js->stick[i]);
    }
    js->input[nr_btn * 2 + nr_stk * 7] = '\0';

    fprintf(fp, "%s\n", js->input);

    if (out_file != NULL) {
        fclose(fp);
    }

    return 0;
}

static int joystick_get_input(struct joystick_struct *js)
{
    int ret;
    struct js_event ev;
    unsigned char num;

    do {
        ret = read(js->fd, &ev, sizeof(struct js_event));
        if (ret != sizeof(struct js_event)) {
            return ERROR("read : %d\n", ret);
        }
    } while (ev.type & JS_EVENT_INIT);

    num = ev.number;
    switch (ev.type) {
    case JS_EVENT_BUTTON:
        if (num < js->nr_buttons) {
            js->button[num] = ev.value;
        }
        break;
    case JS_EVENT_AXIS:
        if (num < js->nr_sticks) {
            js->stick[num] = ev.value;
        }
        break;
    default:
        break;
    }

    return 0;
}

static int joystick_get_info(struct joystick_struct *js)
{
    int ret, fd = js->fd;
#ifdef DEBUG
    char name[64];
#endif /* DEBUG */


    ret = ioctl(fd, JSIOCGBUTTONS, &js->nr_buttons);
    if (ret < 0) {
        return ERROR("ioctl(JSIOCGBUTTONS)\n");
    }
    ret = ioctl(fd, JSIOCGAXES, &js->nr_sticks);
    if (ret < 0) {
        return ERROR("ioctl(JSIOCGAXES)\n");
    }
#ifdef DEBUG
    ret = ioctl(fd, JSIOCGNAME(sizeof(name)), &name);
    if (ret < 0) {
        return ERROR("ioctl(JSIOCGNAME)\n");
    }
    printf("name=\"%s\", buttons=%d, sticks=%d\n", name, js->nr_buttons, js->nr_sticks);
#endif /* DEBUG */


    return 0;
}

static int joystick_init(struct joystick_struct *js, const char *dev_file)
{
    int ret, array_size;
    unsigned char nr_btn, nr_stk;
    unsigned char *p;

    ret = open(dev_file, O_RDONLY);
    if (ret < 0) {
        return ERROR("open(%s)\n", dev_file);
    }
    js->fd = ret;

    ret = joystick_get_info(js);
    if (ret < 0) {
        close(js->fd);
        return ret;
    }
    nr_btn = js->nr_buttons;
    nr_stk = js->nr_sticks;

    array_size = (nr_btn + nr_stk) * sizeof(short);
    p = malloc(array_size + nr_btn * 2 + nr_stk * 7 + 1);
    if (p == NULL) {
        close(js->fd);
        return ERROR("malloc\n");
    }
    js->button = (short *)p;
    js->stick = (short *)&p[nr_btn * sizeof(short)];
    js->input = (char *)&p[array_size];

    return 0;
}

static void joystick_exit(struct joystick_struct *js)
{
    free(js->button);
    close(js->fd);
}

static void joystick_main(const char *dev_file, const char *out_file)
{
    struct joystick_struct js;

    if (joystick_init(&js, dev_file) == 0) {
        do {
            if (joystick_output(&js, out_file) < 0) {
                break;
            }
        } while (joystick_get_input(&js) == 0);

        joystick_exit(&js);
    }
}

/*
 * arg1 : device file (default is /dev/input/js0)
 * arg2 : output file (default is stdout)
 */

int main(int argc, char **argv)
{
    char *dev_file = "/dev/input/js0";
    char *out_file = NULL;

    if (argc >= 2) {
        dev_file = argv[1];
    }
    if (argc >= 3) {
        out_file = argv[2];
    }

    joystick_main(dev_file, out_file);

    return ERROR("exit\n");
}

使用方法としては、
・第一引数にデバイスファイル(省略時は /dev/input/js0)
・第二引数に出力ファイル(省略時は標準出力)
を指定する感じです。
→ "Usage: jsinputd [device-file] [output-file]"

出力フォーマットは ボタン入力は 0 か 1、スティック入力は -32767 ~ 32767 の範囲で出力します。
順番はボタン0、ボタン1・・・とボタンの数だけ出力し、次いでスティック0からスティックの数分出力されます。

とりあえず、引数なしで実行。
$ ./jsinputd
name="ACRUX PS3 wireless GAME PAD", buttons=13, sticks=6
 0 0 0 0 0 0 0 0 0 0 0 0 0      0      0      0      0      0      0
Start ボタンを押したまま、Select ボタンを押して離して、その後 Start ボタンを離した場合。
下記のようにキー入力に変化があった時だけ動作し、出力が出ます。
結構ヌルサクでいい感じです!!(そして、無操作時は CPU 負荷 0 です!)
$ ./jsinputd
name="ACRUX PS3 wireless GAME PAD", buttons=13, sticks=6
 0 0 0 0 0 0 0 0 0 0 0 0 0      0      0      0      0      0      0
 0 0 0 0 0 0 0 0 0 1 0 0 0      0      0      0      0      0      0
 0 0 0 0 0 0 0 0 1 1 0 0 0      0      0      0      0      0      0
 0 0 0 0 0 0 0 0 0 1 0 0 0      0      0      0      0      0      0
 0 0 0 0 0 0 0 0 0 0 0 0 0      0      0      0      0      0      0


ちなみに、ジョイスティックの無線アダプタを Raspberry Pi から抜くと、ioctl で失敗し、終了します。
それ以外だと、基本的には Ctrl-C で中断しない限り、終了しません。

◆ジョイスティックのキー入力検出結果の取り込み

後は、上記で作成した jsinputd の出力結果をシェルで取り込めば、やりたい事は出来そうです。
標準出力に出されても困るので、/tmp/jsinput にでも出力させておこうと思っています。
この jsinputd の出力結果である /tmp/jsinput をシェルで取り込んでアレヤコレヤするわけです。

jsinputd デーモンは起動時にでも、バックグラウンド実行させておく想定です。
("./jsinputd /dev/input/js0 /tmp/jsinput &" みたいな感じで実行すればいいだけです。上記の想定で、ああいうインターフェースにしました。)

ただし、シェルから /tmp/jsinput を取り込むのに、ポーリングしながら読み込んだのでは本末転倒です。

幸いにも、ファイルに変化があった時だけ通知するという inotify という仕組みが Linux カーネルにはあり、それをシェルから利用できるツールがあります。
まずは inotify-tools をインストールしておきましょう。
$ sudo apt-get install -y inotify-tools

ちょっと長くなってきたので、続きは次回

プライバシーポリシー
当サイトでは、Googleを含む第三者配信事業者や広告ネットワークが配信する広告を掲載しています。
当サイトでは、アクセス情報に基づいた適切な広告表示、及びトラフィックデータ収集のためにCookieを使用しています。
ユーザーはCookieの無効化により、これらの情報の提供を拒否する事ができます。
スポンサーリンク
記事検索
スポンサーリンク