ゲームパッドでコマンドを実行 (2017/04/05)

ゲームパッドのボタンを押す組み合わせで Raspberry Pi にコマンドを実行させる仕組みを 2年ほど前に紹介 しましたが、色々問題が見つかったのでアップデートしました。

最近の環境ではゲームパッドを接続してしばらくすると使えなくなる問題、ディスプレイ無しでゲームパッド操作を確認できない問題、掲示板 で yasuo さんに指摘して頂いたコマンドのバックグラウンド実行に伴う問題に対応しました。 Orange Pi Zero でも動作確認しました。


今回も スーファミ風ゲームパッド を使ってテストしました。 700円弱と安いのでおすすめです。


rpi_gamepad.jpg

Raspbian を Jessie にアップデートした後ぐらいから、ゲームパッドを接続してしばらくすると使えなくなるような気がしていました。 今回 systemd-udevd の設定を変更して、jsCommand.lua を systemd の service として動作させるように変更しました。また、ボード上のLEDを点滅させるコマンド を少し変更して、 A, B, A, B というボタン操作でACT(緑) と PWR(赤) のLEDを点滅してゲームパッドの動作確認をできるようにしました。

ボード上のLEDの制御のためRaspberry Pi B+ と Pi Zero 用Raspberry Pi2 と Pi3用の二種類があります。


Raspberry Pi2 と Pi3用のバージョンは、jscommand2.tar.gz (5,180 byte) には次のファイルが含まれています。すべてテキストファイルです。

$ tar ztvvf jscommand2.tar.gz
drwxr-xr-x pi/pi             0 2017-04-03 12:27 jscommand/
-rw-r--r-- pi/pi          1237 2017-04-03 01:30 jscommand/README
-rw-r--r-- pi/pi           258 2017-04-03 12:26 jscommand/jscom.service
-rw-r--r-- pi/pi           102 2017-04-03 12:25 jscommand/70-jscom.rules
-rwxr-xr-x pi/pi          4203 2017-04-03 12:27 jscommand/led.lua
-rwxr-xr-x pi/pi          9641 2017-04-02 19:29 jscommand/jsCommand.lua
-rwxr-xr-x pi/pi           188 2017-04-03 01:28 jscommand/install.sh

Raspberry Pi B+ と Raspberry Pi Zero 用の jscommand2b.tar.gz (5,184 byte) には次のファイルが含まれています。 Raspberry Pi B+ と Raspberry Pi Zero 用は、GPIO レジスタのベースアドレスが Pi2、Pi3 (0x3F200000)と異なって 0x20200000 であるため、led.lua のベースアドレスのみを変更したものです。

$ tar ztvvf jscommand2b.tar.gz
drwxr-xr-x pi/pi             0 2017-04-03 12:29 jscommand/
-rw-r--r-- pi/pi          1237 2017-04-03 01:30 jscommand/README
-rw-r--r-- pi/pi           258 2017-04-03 12:26 jscommand/jscom.service
-rw-r--r-- pi/pi           102 2017-04-03 12:25 jscommand/70-jscom.rules
-rwxr-xr-x pi/pi          4203 2017-04-03 12:29 jscommand/led.lua
-rwxr-xr-x pi/pi          9641 2017-04-02 19:29 jscommand/jsCommand.lua
-rwxr-xr-x pi/pi           188 2017-04-03 01:28 jscommand/install.sh

インストール方法

まず jscommand2.tar.gz (5KB) をダウンロードして下さい。コマンドラインからは 次のコマンドでダウンロードできます。

$ curl -O https://www.mztn.org/rpi/jscommand2.tar.gz

ダウンロードしたデレクトリに移動 (cd ダウンロードしたデレクトリ) した後、以下のコマンドを実行して下さい。 付属の install.sh は jsCommand.lua を /usr/local/bin にコピーし、/etc/udev/rules.d に 70-jscom.rules をコピーします。 その後、service udev reload を実行して 70-jscom.rules の設定を有効にします。 /usr/local/bin にコピーしていますが、/usr/local/ 以下はシステムの管理範囲外(aptコマンドの対象外)ですから大丈夫です。


$  tar zxf jscommand2.tar.gz
$  cd jscommand
$  sudo ./install.sh

使い方

USBの ゲームコントローラを接続すると自動的に jsCommand.lua が実行されます。 ディスプレイが接続されている場合は、コマンドの実行結果がシステムコンソールに表示されます。何も接続されていない場合でも 「コナミコマンド」 (上上下下左右左右BA) でシャットダウンさせることができます。リブートは 上上下下左右左右 [Select] [Start] です。

ゲームコントローラのボタンは次の文字で表しています。

文字 対応するボタン
A [A] ボタン
B [B] ボタン
X [X] ボタン
Y [Y] ボタン
L [L] ボタン
R [R] ボタン
S [START] ボタン
E [SELECT] ボタン
u 十字キーの↑
d 十字キーの↓
l 十字キーの←
r 十字キーの→

デフォルトで設定されている機能

jsCommand.lua を変更していないデフォルトの設定では、次の表のシステム管理用のコマンドが登録されています。デフォルト設定はボタン数の少ないファミコン用のコントローラでも操作できます。システム管理用のコマンドはシャットダウンとリブートを除いて、システム情報を表示するだけのコマンドです。安心して実行できます。


ボタンを押す間隔が1秒以上離れると押したボタンはクリアされ、また最初からになります。 押したボタンがよくわからなくなったら、1秒待てば最初から入力できます。

ボタン操作順 実行されるコマンド
BABABA この表の表示
ABAB 基板上の緑と赤のLEDの点滅
ESESESES jsCommandを終了
uuddlrlrBA /sbin/shutdown -h now シャットダウン
uuddlrlrES /sbin/reboot リブート
SESE /bin/sync
dddd /bin/cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 周波数表示
uuuu /bin/cat /sys/class/thermal/thermal_zone0/temp CPU温度表示
llrr /sbin/ifconfig -a
rrrr /bin/netstat -a
lrlr /usr/bin/uptime
llll /usr/bin/vmstat -s
BBBB /bin/date -R
EEEE /bin/ps aux
SSSS /usr/bin/pstree -A
RRRR /usr/bin/who -a
rrrl /usr/bin/who -a
LLLL /sbin/lsmod
lllr /sbin/lsmod
uldr /bin/uname -a
dudu /bin/df -kT
Eddd /usr/bin/env

上に書いたように 「sudo ./install.sh」を実行しておけば、ゲームパッド/ジョイスティックをUSBポートに接続するだけでログインもすることなく、コマンドを実行できるようになります。自動実行されている場合でも「ESESESES」と SELECTボタンとSTARTボタンを交互に4回ずつ押すと実行している jsCommand.lua は終了します。デスクトップが表示 (Xが起動) されている場合はキーボードの [Ctrl] と [Alt] と [F1] を同時に押すとシステムコンソール(黒い画面)が表示されて、jsCommand.lua の実行結果が表示されます。キーボードの [Ctrl] と [Alt] と [F7] を同時に押すとデスクトップ表示に戻ります。

ヘルプ表示

実行可能なコマンドとボタンの操作順を表示するには次のようにコマンドラインで実行して下さい。表示後すぐに終了します。

$ jsCommand.lua -h

「sudo ./install.sh」を実行して/usr/local/binにインストールしていない場合は、jscommandディレクトリに移って、ピリオドとスラッシュを前につけて実行して下さい。

$ cd jscommand
$ ./jsCommand.lua -h

システムコンソールに表示

Raspberry Pi 起動時にゲームパッド/ジョイスティックが接続されている場合は、jsCommand.lua は自動で起動するようにしていません。次のコマンドで起動して下さい。コマンドの出力はシステムコンソールに表示されます。

$ sudo jsCommand.lua -c &

標準出力に表示

コマンドラインから起動すると、そのまま jsCommand.lua の出力が表示されます。sudo を付けないで実行すると、シャットダウンとリブートはできません。

$ jsCommand.lua

ボタンの入力テスト

ゲームパッドのボタンの表示と jsCommand.lua の認識するボタンとの対応をチェックするには -d オプションを付けて実行します。 押したボタンが表示されるようになります。

$ jsCommand.lua -d

ボタンを押す間隔が1秒以上離れると押したボタンはクリアされ、また最初からになりますが、そのタイムングも試してみてください。

jun@raspberrypi ~/jscommand $ jsCommand.lua -d
Name: USB,2-axis 8-button gamepad  
Ver.: 131328
  No. of Axes    : 2
  No. of Buttons : 8

help : BABABA


AA
AAA
AAAA
X
XXY
XXYY
E
ES
ESES
ESESr
ESESrd
ESESrdul
ESESrdulr
ESESrdulrd
ESESrdulrdES
ESESrdulrdES
ESESrdulrdESX
ESESrdulrdESXR
ESESrdulrdESXRL
B
ES
ESE
ESESE
ESESESE
jun@raspberrypi ~/jscommand $

最後は「ESESESES」と入力していますが、最後の「S」が表示される前に終了しています。

解説

以下は jscommand2.tar.gz に含まれているファイルの解説です。jsCommand.lua をそのまま使用するだけの場合には特に必要ありません。仕組みを知りたい方はどうぞ。

install.sh

インストール用のシェルスクリプトです。jsCommand.lua と led.lua を /usr/local/bin にコピーし、70-jscom.rules を /etc/udev/rules.d にコピーします。 また jscom.service を /etc/systemd/system/ にコピーします。 install.sh は sudo を付けて実行して下さい。

#!/bin/sh

cp -a ./jsCommand.lua /usr/local/bin
cp -a ./led.lua /usr/local/bin
cp -a ./jscom.service /etc/systemd/system/
cp -a ./70-jscom.rules /etc/udev/rules.d/
systemctl daemon-reload

70-jscom.rules

OS が起動している最中にゲームパッド/ジョイスティックをUSBに接続しても jsCommand.lua が自動で実行されるようにする設定ファイルです。 systemd-udevd デーモンが起動する時に読み込みます。 jsCommand.lua が動作していなくても、ゲームパッド/ジョイスティックを接続するだけでシャットダウン等のコマンドを実行できます。

ACTION=="add", SUBSYSTEMS=="usb", KERNEL=="js*",\
TAG+="systemd", ENV{SYSTEMD_WANTS}+="jscom.service"

jscom.service

jsCommand.lua を systemd の service として実行するための設定です。 jsCommand.lua がすでに起動している場合は、ExecStartPre でプロセスを終了させた後に再起動します。 プレフィックスとしてコマンドの前に「-」を付加して、jsCommand.lua プロセスがない場合にも動作を続けるようにしています。 StopWhenUnneeded=yes によって不要な場合には停止させます。

[Unit]
Description = jsCommand
StopWhenUnneeded=yes

[Service]
ExecStartPre = -/usr/bin/killall jsCommand.lua
ExecStart = /usr/local/bin/jsCommand.lua -c &
ExecStop = -/usr/local/bin/killall jsCommand.lua
Type = simple

[Install]
WantedBy=multi-user.target

jsCommand.lua

ゲームコントローラでコマンドを実行するための中心となるプログラムです。Lua という言語のJITコンパイラである luajit-2 用に書かれています。 luajit は Raspbian に最初から入っているため何もインストールする必要はありません。 また、jsCommand.lua は必要なすべての処理を含んでいるため、何にも依存すること無く動作します。 Raspberry Pi に依存していないので Linux なら動作するはずです。掲示板 で yasuo さんに指摘して頂いた変更を加えたので、addCommand で登録するコマンドをバックグラウンドで動作させると停止する不具合を修正しました。


#!/usr/bin/luajit
-- ---------------------------------------------
-- jsCommand.lua  2015/02/28,2017/04/02
--   Copyright (c) 2015-2017 Jun Mizutani,
--   released under the MIT open source license.
-- ---------------------------------------------

local ffi = require("ffi")
local bit = require("bit")
local bnot, bor, band = bit.bnot, bit.bor, bit.band

ffi.cdef    unsigned int time;  /* event timestamp in milliseconds */
  struct js_event {
    unsigned int time;  /* event timestamp in milliseconds */
    short value;        /* value */
    char  type;         /* event type */
    char  number;       /* axis/button number */
  };

  static const int F_SETFL = 4;

  int ioctl(int, unsigned long, ...);
  int open(const char* filename, int flags);
  int read(int fd, void *buf, unsigned int nbytes);
  int fcntl(int fd, int cmd, ...);
  int close(int fd);
  int poll(struct pollfd *fds, unsigned long nfds, int timeout);
]]

local JS_EVENT_BUTTON = 0x1
local JS_EVENT_AXIS   = 0x2
local JS_EVENT_INIT   = 0x80
local JSIOCGVERSION   = 0x80046a01
local JSIOCGAXES      = 0x80016a11
local JSIOCGBUTTONS   = 0x80016a12
local JSIOCGNAME      = 0x80006a13

local EVENT_CLEAR_MSEC   = 1000

local devices = {}
local initialized = false

function init()
  for i = 0, 7 do
    openJoystick(i)
  end
  if #devices > 0 then
    setDeviceInfo()
    initialized = true
  end
  return #devices
end

function open(device)
  local O_RDONLY   = 0
  local O_NONBLOCK = 0x800
  local fd = ffi.C.open(device, O_RDONLY + O_NONBLOCK)
  return fd
end

function openJoystick(n)
  if (n >= 0) and (n <=7) then
    local fd = open("/dev/input/js" .. tonumber(n))
    if fd >= 0 then
      local device = {}
      device.num = n
      device.fd = fd
      table.insert(devices, device)
      devices[n + 1].last_time = 0
      devices[n + 1].event_list = {}
      devices[n + 1].eventStr = {}
      return fd
    else
      return -1
    end
  else
    return -1
  end
end

function setDeviceInfo()
  local version = ffi.new("int[1]")
  local axes = ffi.new("unsigned char[1]")
  local buttons = ffi.new("unsigned char[1]")
  local name = ffi.new("char[128]")
  for i = 1, #devices do
    local fd = devices[i].fd
    ffi.C.ioctl(fd, JSIOCGVERSION, version)
    ffi.C.ioctl(fd, JSIOCGAXES, axes)
    ffi.C.ioctl(fd, JSIOCGBUTTONS, buttons)
    ffi.C.ioctl(fd, JSIOCGNAME + 128 * 0x10000, name)
    devices[i].version = version[0]
    devices[i].num_axes = axes[0]
    devices[i].num_buttons = buttons[0]
    devices[i].name = ffi.string(name)
    devices[i].axes = {}
    devices[i].buttons = {}
    for j = 1, axes[0] do
      table.insert(devices[i].axes,
                   {type = 0, number = 0, value = 0, time = 0})
    end
    for j = 1, buttons[0] do
      table.insert(devices[i].buttons,
                   {type = 0, number = 0, value = 0, time = 0})
    end
  end
end

function readOneEvent(device)
  if (device < 1) or (device > #devices) then
    return nil -- invalid device
  end
  local fd = devices[device].fd
  local js = ffi.new("struct js_event[1]")
  local size = ffi.sizeof(js)
  local res = ffi.C.read(fd, js, size)
  if res == size then
    local event = js[0]
    return event
  else
    return nil
  end
end

function recordEventStr(device, event)
  local js = devices[device]
  local n = event.number
  if event.type == JS_EVENT_BUTTON then
    if n == 0 then      table.insert(js.eventStr, "A")
    elseif n == 1  then table.insert(js.eventStr, "B")
    elseif n == 2  then table.insert(js.eventStr, "X")
    elseif n == 3  then table.insert(js.eventStr, "Y")
    elseif n == 4  then table.insert(js.eventStr, "L")
    elseif n == 5  then table.insert(js.eventStr, "R")
    elseif n == 6  then table.insert(js.eventStr, "E")
    elseif n == 7  then table.insert(js.eventStr, "S")
    end
  elseif event.type == JS_EVENT_AXIS then
    local v = event.value
    if n == 0 then
      if v > 0 then     table.insert(js.eventStr, "r")
      elseif v < 0 then table.insert(js.eventStr, "l")
      end
    elseif n == 1 then
      if v > 0 then     table.insert(js.eventStr, "d")
      elseif v < 0 then table.insert(js.eventStr, "u")
      end
    end
  end
end

function clearEventStr(device)
  devices[device].eventStr = {}
end

function getEventStr(device)
  return table.concat(devices[device].eventStr)
end

function readAllEvents(device)
  local event
  local numEvent = 0
  local js = devices[device]
  repeat
    event = readOneEvent(device)
    if event ~= nil then
      numEvent = numEvent + 1
      if (event.time - js.last_time) > EVENT_CLEAR_MSEC then
        clearEventStr(device)
      end
      local num = event.number + 1
      if band(event.type, JS_EVENT_INIT) > 0 then
        js.start = event.time;
        event.type = band(event.type, bnot(JS_EVENT_INIT))
        if event.type == JS_EVENT_BUTTON then
          js.buttons[num].value = event.value
          js.buttons[num].time = event.time
        else
          js.axes[num].value = event.value
          js.axes[num].time = event.time
        end
      elseif event.type == JS_EVENT_BUTTON then
        local button =  js.buttons[num]
        if event.value == 0 then
          button.longPush = event.time - button.time
          button.longRelease = 0
        else
          button.longPush = 0
          button.longRelease = event.time - button.time
          recordEventStr(device, event)
        end
        button.value = event.value
        button.time = event.time
      else
        js.axes[num].value = event.value
        js.axes[num].time = event.time
        if event.value ~= 0 then
          recordEventStr(device, event)
        end
      end
      js.last_time = event.time
    end
  until event == nil
  return numEvent
end

function readAllDevices()
  for i = 1, #devices do
    readAllEvents(i)
  end
end

function exec(command)
  local f = io.popen(command .. " 2>&1", "r")
  if f ~= nil then
    -- Check if the command is running in the background. (by yasuo) 
    if string.find(command, "&") ~= nil then
     return nil -- command is running in the background. 
    end
    local result = f:read('*a')  -- read output
    f:close()
    return result
  else
    return "execute error"
  end
end

function initCommandList()
  commandList = { BABABA = "help" }
end

function addCommand(keys, command)
  commandList[keys] = command
  return #commandList
end

function listCommand(f)
  f:write("[A] [B] [X] [Y] [L] [R] [Start] [sElect], ")
  f:write("up, down, left, right\n")
  for key, command in pairs(commandList) do
    f:write(key .. " : " .. command .. "\n")
  end
end

function execCommand(numDevice, str, f)
  for key, command in pairs(commandList) do
    if str == key then
      if command == "quit" then os.exit() end
      if command == "help" then
        listCommand(f)
        clearEventStr(numDevice)
      else
        f:write("\nkey = ".. key .. ", command = " .. command .. "\n")
        local result = exec(command)
        if result ~= nil then f:write(result) end
        clearEventStr(numDevice)
      end
    end
  end
end

function checkCommand(numDevice, f)
  local numEvent = readAllEvents(numDevice)
  if numEvent > 0 then
    str = getEventStr(numDevice)
    execCommand(numDevice, str, f)
    return str
  end
  return nil
end

function getNoOfDevices()
  return #devices
end

-- device_num: 1..8
function getName(device_num)
  return devices[device_num].name
end

function getVersion(device_num)
  return devices[device_num].version
end

function getNoOfAxes(device_num)
  return devices[device_num].num_axes
end

function getNoOfButtons(device_num)
  return devices[device_num].num_buttons
end

function sleep(sec)
  ffi.C.poll(nil, 0, sec * 1000)
end

function printf(f, fmt, ...)
  f:write(string.format(fmt, ...))
end

function deviceList(f)
  local num_device = getNoOfDevices()
  for i = 1, num_device do
    printf(f, "Name: %s\n", getName(i))
    printf(f, "Ver.: %s\n", getVersion(i))
    local num_axes = getNoOfAxes(i)
    printf(f, "  No. of Axes    : %d\n", num_axes)
    local num_buttons = getNoOfButtons(i)
    printf(f, "  No. of Buttons : %d\n", num_buttons)
  end
  printf(f, "\nhelp : BABABA\n\n")
end

-- -------------
--     main
-- -------------
  local numOfDevices = init()
  local str
  local f
  if arg[1] == "-d" then DEBUG = true end
  if arg[1] == "-c" then CONSOLE = true end
  if CONSOLE then
    f = io.open("/dev/console", "w")
  else
    f = io.open("/dev/stdout", "w")
  end

  initCommandList()
 
  -- A, B, X, Y, L, R, Start, sElect, up, down, left, right
  addCommand("ESESESES", "quit")
  addCommand("uuddlrlrBA", "/sbin/shutdown -h now")
  addCommand("uuddlrlrES", "/sbin/reboot")
  addCommand("BBBB", "/bin/date -R")
  addCommand("SSSS", "/usr/bin/pstree -A")
  addCommand("SESE", "/bin/sync")
  addCommand("EEEE", "/bin/ps aux")
  addCommand("LLLL", "/sbin/lsmod")
  addCommand("lllr", "/sbin/lsmod")
  addCommand("RRRR", "/usr/bin/who -a")
  addCommand("rrrl", "/usr/bin/who -a")
  addCommand("rrrr", "/bin/netstat -a")
  addCommand("llrr", "/sbin/ifconfig -a")
  addCommand("llll", "/usr/bin/vmstat -s")
  addCommand("lrlr", "/usr/bin/uptime")
  addCommand("uuuu", "/bin/cat /sys/class/thermal/thermal_zone0/temp")
  addCommand("dddd",
    "/bin/cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq")
  addCommand("dudu", "/bin/df -kT")
  addCommand("uldr", "/bin/uname -a")
  addCommand("Eddd", "/usr/bin/env")
  addCommand("ABAB", "/usr/local/bin/led.lua")
  --
  if arg[1] == "-h" then
    listCommand(f)
    os.exit()
  end

  if numOfDevices > 0 then
    deviceList(f)
    while (true) do
      str = checkCommand(1, f)
      if DEBUG and str ~= nil then io.write(str.."\n") end
      sleep(0.5)
    end
  end

led.lua

ACT(緑) と PWR(赤) のLEDを点滅するプログラムです。 Raspberry Pi 専用です。 Raspberry Pi B+ と Raspberry Pi Zero 用は、GPIO レジスタのベースアドレスを 0x20200000 に設定します。 Raspberry Pi2と Pi3 は レジスタのベースアドレスに 0x3F200000 を指定します。

#!/usr/bin/luajit
-- ---------------------------------------------
-- led.lua     2017/04/03
--   Copyright (c) 2015-2017 Jun Mizutani,
--   released under the MIT open source license.
-- ---------------------------------------------

local bit = require("bit")
local ffi = require("ffi")
local C = ffi.C

ffi.cdef  int munmap(void *addr, size_t len);
  void *mmap(void *addr, size_t len, int prot, int flags, int fd, int offset);
  int munmap(void *addr, size_t len);
  int open(const char *pathname, int flags, int mode);
  typedef unsigned long int nfds_t;
  int poll(struct pollfd *fds, nfds_t nfds, int timeout);
]]

function sleep(sec)
  C.poll(nil, 0, sec * 1000)
end

-- 物理アドレス空間のデバイスファイルをオープン
function mem_open()
  local O_RDONLY   = 0
  local O_WRONLY   = 1
  local O_RDWR     = 2
  local fd = C.open("/dev/mem", O_RDWR, 0)
  return fd
end

-- メモリをマッピング
function mem_map(fd, addr, length, offset)
  local PROT_READ  = 1
  local PROT_WRITE = 2
  local PROT_EXEC  = 4
  local MAP_SHARED = 1
  local MAP_PRIVATE = 2
  if fd > 0 then
    local p = C.mmap(addr, length, PROT_READ + PROT_WRITE, MAP_SHARED,
                     fd, offset)
    local mem = ffi.cast("int32_t *", p)
    if mem > ffi.cast("int32_t *", 0) then
      return mem
    end
  end
  return nil
end

-- メモリのマッピングを解除
function mem_unmap(addr, length)
  local p = ffi.cast("int32_t *", addr)
  return C.munmap(p, length)
end

local gpio
-- 初代 Raspberry Pi 用のベースアドレス
-- local BCM_PEREIFERAL_ADDR = 0x20200000
-- Raspberry Pi2 用のベースアドレス
local BCM_PEREIFERAL_ADDR = 0x3F200000

local GPSET0 = 7
local GPSET1 = 8
local GPCLR0 = 10
local GPCLR1 = 11
local GPLEV0 = 13
local GPLEV1 = 14

-- GPIO のレジスタを gpio 配列に設定
function gpioOpen()
  local fd = mem_open()
  local mem = mem_map(fd, nil, 256, BCM_PEREIFERAL_ADDR)
  if mem  == nil then
    print("You must run 'sudo luajit gpio.lua'.")
  end
  gpio = mem
end

-- GPIO のレジスタのマッピングを解除
function gpioClose()
  mem_unmap(BCM_PEREIFERAL_ADDR, 256)
end

-- pinNo の GPIO のモードを設定
-- mode: 0/input, 1/output
function gpioSetPinMode(pinNo, mode)
  if pinNo > 53 or pinNo < 0 then
    print("PinNo is out of range.")
    return
  elseif mode > 7 then
    print("Mode must be [0..7].")
    return
  else
    local reg = math.floor(pinNo / 10)
    local shift = (pinNo % 10) * 3
    local mask = bit.bnot(bit.lshift(7, shift)) -- ~(7 << shift)
    local val = bit.lshift(mode, shift)
    local orig = gpio[reg]
    local new = bit.bor(bit.band(orig, mask), val)
    gpio[reg] = new
  end
end

-- pinNo の GPIO のモードを取得
function gpioGetPinMode(pinNo)
  if pinNo > 53 or pinNo < 0 then
    print("PinNo is out of range.")
    return nil
  else
    local reg = math.floor(pinNo / 10)
    local shift = (pinNo % 10) * 3
    local mask = bit.lshift(7, shift)
    local val = bit.band(gpio[reg], mask)
    local mode = bit.rshift(val, shift)
    return mode
  end
end

-- pinNo の GPIO に 1 を出力
function gpioSet(pinNo)
  if pinNo < 32 then
    gpio[GPSET0] = bit.lshift(1, pinNo)
  elseif pinNo < 54 then
    gpio[GPSET1] = bit.lshift(1, pinNo - 32)
  end
end

-- pinNo の GPIO に 0 を出力
function gpioClear(pinNo)
  if pinNo < 32 then
    gpio[GPCLR0] = bit.lshift(1, pinNo)
  elseif pinNo < 54 then
    gpio[GPCLR1] = bit.lshift(1, pinNo - 32)
  end
end

-- pinNo の GPIO を読み出し
function gpioRead(pinNo)
  local val
  if pinNo < 32 then
    val = bit.band(gpio[GPLEV0], bit.lshift(1, pinNo))
  elseif pinNo < 54 then
    val = bit.band(gpio[GPLEV1], bit.lshift(1, pinNo - 32))
  end
  if val ~= 0 then
    return 1
  end
  return 0
end

-- PWR(赤)と ACT(緑)を点滅させる
gpioOpen()             -- GPIO アクセス準備
gpioSetPinMode(35, 1)  -- PWR : 出力モード
gpioSetPinMode(47, 1)  -- ACT : 出力モード
for i = 1, 20 do
  gpioSet(35)          -- PWR を 点灯
  gpioSet(47)          -- ACT を 点灯
  sleep(0.1)           -- 0.1秒間待つ
  gpioClear(35)        -- PWR を 消灯
  gpioClear(47)        -- ACT を 消灯
  sleep(0.1)           -- 0.1秒間待つ
end
gpioClose()

動作確認

ゲームパッドの接続と取り外しに伴って、jsCommand.lua が正常に起動/停止されることを確認します。 Raspbian Jessie でカーネルは現時点で最新の以下の環境で確認しました。

機種

Raspberry Pi 2

$ uname -a
Linux raspi_jessie 4.9.20-v7+ #985 SMP Mon Apr 3 10:30:44 BST 2017 armv7l GNU/Linux

Raspberry Pi B+

$ uname -a
Linux raspberrypi 4.1.19+ #858 Tue Mar 15 15:52:03 GMT 2016 armv6l GNU/Linux

起動時の状態

ゲームパッド接続状態で起動

$ systemctl status jscom.service
 jscom.service - jsCommand
   Loaded: loaded (/etc/systemd/system/jscom.service; disabled)
   Active: active (running) since Mon 2017-04-03 11:26:03 JST; 38s ago
  Process: 363 ExecStartPre=/usr/bin/killall jsCommand.lua (code=exited, status=1/FAILURE)
 Main PID: 437 (jsCommand.lua)
   CGroup: /system.slice/jscom.service
           └─437 /usr/bin/luajit /usr/local/bin/jsCommand.lua -c &

再接続

ゲームパッドを取りはずした後に状態を確認します。プロセスIDが異っているため、jsCommand.lua が再起動していることが確認できます。

$ systemctl status jscom.service
 jscom.service - jsCommand
   Loaded: loaded (/etc/systemd/system/jscom.service; disabled)
   Active: active (running) since Mon 2017-04-03 11:27:25 JST; 13s ago
  Process: 868 ExecStartPre=/usr/bin/killall jsCommand.lua (code=exited, status=1/FAILURE)
 Main PID: 871 (jsCommand.lua)
   CGroup: /system.slice/jscom.service
           └─871 /usr/bin/luajit /usr/local/bin/jsCommand.lua -c &

接続解除

ゲームパッドを取りはずした状態を確認します。 jsCommand.lua のプロセスは動作していません。

$ systemctl status jscom.service
  jscom.service - jsCommand
   Loaded: loaded (/etc/systemd/system/jscom.service; disabled)
   Active: inactive (dead)

再再接続

再度ゲームパッドを接続して確認します。別のプロセスIDで、jsCommand.lua が動作していることが確認できます。

$ systemctl status jscom.service
  jscom.service - jsCommand
   Loaded: loaded (/etc/systemd/system/jscom.service; disabled)
   Active: active (running) since Mon 2017-04-03 11:28:43 JST; 10s ago
  Process: 904 ExecStartPre=/usr/bin/killall jsCommand.lua (code=exited, status=1/FAILURE)
 Main PID: 907 (jsCommand.lua)
   CGroup: /system.slice/jscom.service
           └─907 /usr/bin/luajit /usr/local/bin/jsCommand.lua -c &

Orange Pi Zero でも確認

おまけとして Orange Pi Zero でも動作確認してみました。 OS は公式のイメージではなく Armbian (Ubuntu 16.04.1)を使っています。

jun@orangepizero:~$ uname -a
Linux orangepizero 3.4.113-sun8i #28 SMP PREEMPT Thu Feb 2 02:01:28 CET 2017 armv7l armv7l armv7l GNU/Linux

超小型の Orange Pi Zero をサーバとして使っていると、シャットダウンのためにディスプレイやキーボードをつなぐのはバカバカしくなります。ネットワーク越しに ssh で接続するより、ゲームパッドを接続して「上上下下左右左右BA」するほうが楽です。でも、もっと小さいゲームパッドが欲しくなりますね。


OPi0_gamepad.jpg

LED の点滅コマンドはRaspberry Pi のハードウェアに依存するため動作しません。

jun@orangepizero:~$ tar zxf jscommand2.tar.gz 
jun@orangepizero:~$ cd jscommand/
jun@orangepizero:~/jscommand$ sudo ./install.sh

jun@orangepizero:~/jscommand$ systemctl status jscom.service
 jscom.service - jsCommand
   Loaded: loaded (/etc/systemd/system/jscom.service; disabled; vendor preset: e
   Active: inactive (dead)
jun@orangepizero:~/jscommand$ systemctl status jscom.service
 jscom.service - jsCommand
   Loaded: loaded (/etc/systemd/system/jscom.service; disabled; vendor preset: e
   Active: active (running) since Tue 2017-04-04 15:30:31 UTC; 7s ago
  Process: 5547 ExecStartPre=/usr/bin/killall jsCommand.lua (code=exited, status
 Main PID: 5549 (jsCommand.lua)
   CGroup: /system.slice/jscom.service
           └─5549 /usr/bin/luajit /usr/local/bin/jsCommand.lua -c &

Raspberry Pi にしても、Orange Pi にしても色々な進化が速くて、遊ぶネタは尽きません。 脳もマルチコアにできませんかね (笑)。

続く...



このページの目次