Raspberry Pi用node.jsでゲームパッド入力をキーボード/マウス入力に変換する

# 概要

Raspberry Piでゲームパッドの入力をキーボードやマウスの入力に変換するソフトは今の所無いようなので、node.jsでさくっと作ってみた。

(Intel/AMD製CPU用の)Linux・Windows・MacにはGUIで設定できる同様のソフトが存在するので素直にそっちを使ったほうがいいです。

# インストール

node.js及び、joystick・robotjsモジュールをインストールする必要があります。
また、robotjsをインストールするためにlibx11-devとlibxtst-devもインストールする必要があるようです([参考](https://qiita.com/n0bisuke/items/2075ab1c72e59ac74e12))

```sh
sudo apt install npm libx11-dev libxtst-dev
npm install joystick robotjs --save-dev
```

# スクリプト

joystickモジュールでゲームパッドの入力を受け取ってその結果を元にrobotjsモジュールを利用してキー・マウス操作する感じになります。

接続されるまでポーリングで待機したり、キー・マウス入力への変換をオブジェクトで定義したりできるようにしてみました。

```joykey.js
const fs = require('fs');
const joystick = require('joystick');
const robot = require('robotjs');

//ジョイスティックのID(通常は0)
var jid = 0;

var joys = new joystick(jid);

//コンソールにジョイスティックイベントの内容を表示
//var consolelog = true;
var consolelog = false;

//キーやスティックが押されたときの動作を定義
/*
ジョイスティックのボタン押下(button)
"0", "1", "2"....

ジョイスティックのスティック操作(axis)
"a0", "a1", "a2"...


type: "enable"    キー変換の有効・無効の切り替え(無効でもenableだけは有効)

type: "key"       キークリック
    key:      クリックするキー文字もしくはキー文字列

type: "text"      文字列送信
    str:      送信する文字列

type: "mouse"     マウスボタン操作
    mtype:    マウスボタンの操作タイプ("ckick", "toggle", "double")
    button:   クリックするマウスボタン("left", "middle", "right")

type: "move"      マウス移動
    mx:       x軸移動量
    my:       y軸移動量

type: "scroll"    スクロール
    mx:       x軸スクロール量
    my:       y軸スクロール量

type: "updown"    上下カーソルキー
    mv:       0以上:そのまま　0未満:反転 

type: "leftright" 左右カーソルキー
    mv:       0以上:そのまま　0未満:反転 

*/
var joytable = {
    "0": { type: "mouse", mtype: "click", button: "right" },
    "1": { type: "mouse", mtype: "click", button: "left" },
    "2": { type: "toggle", key: "left" },
    "a0": { type: "move", mx: 10, my: 0 },
    "a1": { type: "move", mx: 0, my: 10 },
    "a3": { type: "scroll", mx: 0, my: 5 },
    "a4": { type: "leftright", mv: 0},
    "a5": { type: "updown", mv: 0},
};

//toggle用にマウスボタンの状態を記憶(trueが押されている状態)
var mousestate = {
    "left": false,
    "right": false,
    "middle": false
};

//キーマウス動作が有効かどうか
var enable = true;

//定義を元にキーやマウスの動作を実行
var sendkey = function (dat, mul) {
    if (dat == null || dat.type == null) return;

    mul = mul < 0 ? -1 : 1;

    if (dat.type === "enable") {
        //キー変換有効無効の切り替え
        enable = enable ? false : true;
    } else if (enable && dat.type === "key") {
        //キー送信
        if (dat.key == null) return;
        robot.keyTap(dat.key);
    } else if (enable && dat.type === "text") {
        //文字列送信
        if (dat.str == null) return;
        robot.typeString(dat.str);
    } else if (enable && dat.type === "mouse") {
        if (dat.mtype === "click") {
            //マウスボタンクリック
            if (dat.button == null) return;
            robot.mouseClick(dat.button);
        } else if (dat.mtype === "double") {
            //マウスボタンダブルクリック
            if (dat.button == null) return;
            robot.mouseClick(dat.button, true);
        } else if (dat.mtype === "toggle") {
            //マウスボタン押下状態切り替え
            if (dat.button == null || mousestate[dat.button] == null) return;
            robot.mouseToggle(mousestate[dat.button] ? "up" : "down", dat.button);
            mousestate[dat.button] = mousestate[dat.button] ? false : true;
        }
    } else if (enable && dat.type === "move") {
        //マウスポインタ移動
        if (dat.mx == null || dat.my == null) return;
        var pos = robot.getMousePos();
        robot.moveMouseSmooth(pos.x + (dat.mx * mul), pos.y + (dat.my * mul));
    } else if (enable && dat.type === "scroll") {
        //スクロール
        if (dat.mx == null || dat.my == null) return;
        robot.scrollMouse(dat.mx * mul, dat.my * mul);
    } else if (enable && dat.type === "updown") {
        //上下カーソルキー送信
        if (dat.mv == null) return;
        dat.mv = dat.mv < 0 ? -1 : 1;
        robot.keyTap(dat.mv * mul < 0 ? "up" : "down");
    } else if (enable && dat.type === "leftright") {
        //左右カーソルキー送信
        if (dat.mv == null) return;
        dat.mv = dat.mv < 0 ? -1 : 1;
        robot.keyTap(dat.mv * mul < 0 ? "left" : "right");
    }
};

//ジョイスティック接続待ち用のsleep
var sleep = function (waitSec) {
    return new Promise(function (resolve) {
        setTimeout(function () { resolve() }, waitSec);
    });
};

//joystickイベントの定義
var init = function (joy) {
    //ボタンクリックイベント
    joy.on('button', button => {
        if (consolelog) console.log({ button });
        if (button.value != 0) {
            sendkey(joytable[button.number], 0);
        }
    });

    //axis移動イベント
    joy.on('axis', axis => {
        if (consolelog) console.log({ axis });
        if (axis.value != 0) {
            sendkey(joytable["a" + axis.number], axis.value);
        }
    });

    //エラーイベント
    joy.on('error', async (err) => {
        if (consolelog) console.log({ err });
        //エラーが発生したら/dev/input/js?の存在を1秒ごとに確認して存在すれば再度joystickオブジェクトを作成
        while (1) {
            if (fs.existsSync('/dev/input/js' + jid)) break;
            await sleep(1000);
        }
        joys = new joystick(jid);
        init(joys)
    });
};

//開始
init(joys);

```

# キーリピート対応版

このまんまだとキーリピートができないのでちょっと使いにくいが、~~対応しようと思うと根本的に作り直す感じになるっぽいので保留・・・~~
こっちの方もさくっと作ってみた。

```joykeyrepeat.js
const fs = require('fs');
const joystick = require('joystick');
const robot = require('robotjs');

//ジョイスティックのID(通常は0)
var jid = 0;

var joys = new joystick(jid);

//コンソールにジョイスティックイベントの内容を表示
//var consolelog = true;
var consolelog = false;

//キーやスティックが押されたときの動作を定義
/*
ジョイスティックのボタン押下(button)
"0", "1", "2"....

ジョイスティックのスティック操作(axis)
"a0", "a1", "a2"...


type: "enable"    キー変換の有効・無効の切り替え(無効でもenableだけは有効)
    repeat:   キーリピート(true:有効, false:無効)

type: "key"       キークリック
    key:      クリックするキー文字もしくはキー文字列
    repeat:   キーリピート(true:有効, false:無効)

type: "text"      文字列送信
    str:      送信する文字列
    repeat:   キーリピート(true:有効, false:無効)

type: "mouse"     マウスボタン操作
    mtype:    マウスボタンの操作タイプ("ckick", "toggle", "double")
    button:   クリックするマウスボタン("left", "middle", "right")
    repeat:   キーリピート(true:有効, false:無効)

type: "move"      マウス移動
    mx:       x軸移動量
    my:       y軸移動量
    repeat:   キーリピート(true:有効, false:無効)

type: "scroll"    スクロール
    mx:       x軸スクロール量
    my:       y軸スクロール量
    repeat:   キーリピート(true:有効, false:無効)

type: "updown"    上下カーソルキー
    mv:       0以上:そのまま　0未満:反転 
    repeat:   キーリピート(true:有効, false:無効)

type: "leftright" 左右カーソルキー
    mv:       0以上:そのまま　0未満:反転 
    repeat:   キーリピート(true:有効, false:無効)

*/
var joytable = {
    "0": { type: "mouse", mtype: "click", button: "right", repeat: false},
    "1": { type: "mouse", mtype: "click", button: "left", repeat: false},
    "2": { type: "toggle", key: "left", repeat: false},
    "a0": { type: "move", mx: 10, my: 0, repeat: true},
    "a1": { type: "move", mx: 0, my: 10, repeat: true},
    "a3": { type: "scroll", mx: 0, my: 5, repeat: true},
    "a4": { type: "leftright", mv: 0, repeat: true},
    "a5": { type: "updown", mv: 0, repeat: true},
};

//ジョイスティックの状態を記憶
var joystate = {
    "0": 0, "1": 0, "2": 0, "3": 0, "4": 0,
    "5": 0, "6": 0, "7": 0, "8": 0, "9": 0,
    "10": 0, "11": 0, "12": 0, "13": 0, "14": 0,
    "15": 0, "16": 0, "17": 0, "18": 0, "19": 0,
    "a0": 0, "a1": 0, "a2": 0, "a3": 0, "a4": 0,
    "a5": 0, "a6": 0, "a7": 0, "a8": 0, "a9": 0,
};

var joyenable = false;

//toggle用にマウスボタンの状態を記憶(trueが押されている状態)
var mousestate = {
    "left": false,
    "right": false,
    "middle": false
};

//キーマウス動作が有効かどうか
var enable = true;

//定義を元にキーやマウスの動作を実行
var sendkey = function (dat, mul) {
    if (dat == null || dat.type == null) return;

    mul = mul < 0 ? -1 : 1;

    if (dat.type === "enable") {
        //キー変換有効無効の切り替え
        enable = enable ? false : true;
    } else if (enable && dat.type === "key") {
        //キー送信
        if (dat.key == null) return;
        robot.keyTap(dat.key);
    } else if (enable && dat.type === "text") {
        //文字列送信
        if (dat.str == null) return;
        robot.typeString(dat.str);
    } else if (enable && dat.type === "mouse") {
        if (dat.mtype === "click") {
            //マウスボタンクリック
            if (dat.button == null) return;
            robot.mouseClick(dat.button);
        } else if (dat.mtype === "double") {
            //マウスボタンダブルクリック
            if (dat.button == null) return;
            robot.mouseClick(dat.button, true);
        } else if (dat.mtype === "toggle") {
            //マウスボタン押下状態切り替え
            if (dat.button == null || mousestate[dat.button] == null) return;
            robot.mouseToggle(mousestate[dat.button] ? "up" : "down", dat.button);
            mousestate[dat.button] = mousestate[dat.button] ? false : true;
        }
    } else if (enable && dat.type === "move") {
        //マウスポインタ移動
        if (dat.mx == null || dat.my == null) return;
        var pos = robot.getMousePos();
        robot.moveMouseSmooth(pos.x + (dat.mx * mul), pos.y + (dat.my * mul));
    } else if (enable && dat.type === "scroll") {
        //スクロール
        if (dat.mx == null || dat.my == null) return;
        robot.scrollMouse(dat.mx * mul, dat.my * mul);
    } else if (enable && dat.type === "updown") {
        //上下カーソルキー送信
        if (dat.mv == null) return;
        dat.mv = dat.mv < 0 ? -1 : 1;
        robot.keyTap(dat.mv * mul < 0 ? "up" : "down");
    } else if (enable && dat.type === "leftright") {
        //左右カーソルキー送信
        if (dat.mv == null) return;
        dat.mv = dat.mv < 0 ? -1 : 1;
        robot.keyTap(dat.mv * mul < 0 ? "left" : "right");
    }
};

//ジョイスティック接続待ち、ループ用のsleep
var sleep = function (waitSec) {
    return new Promise(function (resolve) {
        setTimeout(function () { resolve() }, waitSec);
    });
};

//joystickイベントの定義
var init = function (joy) {
    //ボタンクリックイベント
    joy.on('button', button => {
        if (consolelog) console.log({ button });
        joystate[button.number] = button.value;
    });

    //axis移動イベント
    joy.on('axis', axis => {
        if (consolelog) console.log({ axis });
        joystate['a' + axis.number] = axis.value;
    });

    //エラーイベント
    joy.on('error', async (err) => {
        if (consolelog) console.log({ err });
        //エラーが発生したら/dev/input/js?の存在を1秒ごとに確認して存在すれば再度joystickオブジェクトを作成
        joyenable = false;
        while (1) {
            if (fs.existsSync('/dev/input/js' + jid)) break;
            await sleep(1000);
        }
        joys = new joystick(jid);
        init(joys)
    });
    joyenable = true;
};

//ループ
var start = async function () {
    while(1) {
        if(joyenable) {
            for(var key in joytable) {
                if(joystate[key] != 0){
                    sendkey(joytable[key], joystate[key]);
                    if(!joytable[key].repeat) joystate[key] = 0;
                }
            }
        }
        await sleep(16);
    }
}

//開始
init(joys);
start();
```