主題

【程式】讀取手把輸入 (Linux)

Shark | 2022-04-12 12:38:32 | 巴幣 214 | 人氣 156

前篇:讀取鍵盤與滑鼠輸入 (X Window)
本篇介紹手把輸入。

程式教學目錄



本篇的程式沒有透過X Window,而是從Linux底層讀取,因為X Window沒有讀取手把輸入的功能。
在寫程式之前,先看看作業系統內部的手把訊息是什麼。

各種硬體設備在Linux裡會有檔案對應,這些檔案不是儲存裝置上的資料(磁碟、flash記憶體等等),而是系統自動產生的虛擬檔案,讀寫檔案就是把資料從裝置輸入輸出。輸入裝置對應的檔案放在/dev/input/資料夾內。

把手把的線插進USB孔或設定好藍牙連接後,打這個指令,或用GUI的檔案瀏覽器開這個資料夾看有沒有js開頭的檔案:
ls /dev/input/js*

連接兩個手把則會出現兩個檔案,沒出現就表示作業系統沒偵測到手把。


打這個指令,如果剛剛找到的檔案不是js0就自己改一下指令。
筆者寫這篇時用的Fedora 33是剛灌好就有內建jstest,如果您使用的發行版沒有內建,自己找一下套件。
jstest /dev/input/js0

之後按按鈕或動類比桿,就會顯示手把的狀態。

打jstest不加參數會顯示這個指令的其他用法,有興趣自己試試。

也有一些圖形介面工具可以看手把輸入,例如jstest-gtk。


或KDE桌面的「系統設定→輸入裝置→遊戲控制器」。


Linux的手把輸入有兩種資料
  1. 按鈕(Button)
    每個按鈕都是一個on、off的開關,1 bit就可以描述。
    有些手把的類比桿可以按下,這也算是按鈕。
  2. 軸(Axis)
    十字鈕與類比桿屬於這種。值是-32767~32767,代表推類比桿的程度,中間位置是0,最左或最上是-32767,最右、下是32767。
    十字鈕雖然沒有力度控制但也是當成軸處理,數值只有-32767、0、32767三種情況。
    有的手把有類比按鈕(例如Xbox手把),按得淺或深效果不一樣,也是視為軸處理。
跟Windows版不同的是沒有POV(視覺頭盔),因此不用想辦法把POV還原成十字鈕。

這是Linux版的snes9x按鈕設定

按十字鈕為何這裡顯示的是Axis而不是button?要看內部資料才能解答。
右邊畫面的Joystick Axis Threshold是把類比桿當成按鈕時,傾斜多少程度才判定是按下按鈕。



這次不需要建視窗,寫個命令列程式就行了。

joystick.c
#include<stdio.h>
#include<linux/joystick.h>
#include<glob.h>
#include<fcntl.h>
#include<unistd.h>
#include<errno.h>


const int SLEEP_INTERVAL=20*1000; //50 FPS

//傳回手把的file descriptor

static int findJoystick(){
  glob_t globRes;
  int joyFd=-1;
  glob("/dev/input/js*",0,NULL,&globRes);
  for(int i=0; i<globRes.gl_pathc; i++){
    joyFd=open(globRes.gl_pathv[i], O_RDONLY|O_NONBLOCK);
    if(joyFd>=0){
      printf("--find joystick %s\n", globRes.gl_pathv[i]);
      break;
    }
  }
  globfree(&globRes);
  if(joyFd<0){
    return -1;
  }

  //印出手把資訊
  char name[32];
  name[0]=0; //如果ioctl()失敗,防止printf()的%s出問題
  char buttonNum,axisNum;
  ioctl(joyFd,JSIOCGNAME(32), name);
  ioctl(joyFd,JSIOCGBUTTONS, &buttonNum);
  ioctl(joyFd,JSIOCGAXES, &axisNum);
  printf("name:%s\nbuttons:%d axes:%d\n", name,buttonNum,axisNum);
  return joyFd;
}

//手把拔出則傳回0,此時要重新尋找手把
static int readJoystickEvent(int joyFd){
  struct js_event jsEvent;
  while(1){
    int ret=read(joyFd, &jsEvent, sizeof(jsEvent));
    if(ret<0){
      if(errno!=EAGAIN){ //EAGAIN為目前沒資料,其餘errno為讀不到手把
        close(joyFd);
        printf("--joystick is removed\n");
        return 0;
      }
      return 1;
    }

    switch(jsEvent.type){
    case JS_EVENT_AXIS|JS_EVENT_INIT:
    case JS_EVENT_AXIS:
      printf("axis %d value %d\n", jsEvent.number, jsEvent.value);
      break;
    case
JS_EVENT_BUTTON|JS_EVENT_INIT:
    case JS_EVENT_BUTTON:
      printf("button %d value %d\n", jsEvent.number, jsEvent.value);
      break;
    }
  }
}

int main(){
  int joyFd=-1;
  for(;;usleep(SLEEP_INTERVAL)){
    //一、尋找手把
    joyFd=findJoystick();
    if(joyFd==-1){
      continue;
    }

    //二、讀取手把輸入
    //手把拔出才離開這個迴圈

    while(readJoystickEvent(joyFd)){
      usleep(SLEEP_INTERVAL);
    }
    joyFd=-1;
  }
  return 0;
}

用無窮迴圈每隔20ms跑一次邏輯。分成兩個部分,第一部分偵測現在有沒有連接手把,如果有則把file descriptor記下來,第二部分用另一個迴圈讀取輸入。本程式可以偵測手把拔出再重新插入的情況。
為了避免縮排過多,主要邏輯寫在兩個函式裡,把程式碼嵌入main()裡面也可以達到相同效果。main()裡的邏輯跟Windows版類似,只有兩個函式內容不一樣。
如果連接兩個以上的手把,本程式只能讀取其中一個,如何讀取複數個手把就請自己研究。

本程式沒有離開迴圈的方法,請按Ctrl+C或Ctrl+Break結束程式。

首先找到這個檔案,本篇用到的常數和struct定義要在這裡查:
/usr/include/linux/joystick.h
這篇kernel的文件也可以參考。
https://www.kernel.org/doc/html/latest/input/joydev/joystick-api.html

一、尋找手把
復習一下這篇,前面說到Linux會在/dev/input產生檔案對應到手把,尋找手把就是用檔案操作的函式開啟檔案。
檔案操作—Linux篇
用glob()搜尋這個資料夾裡js開頭的檔案,然後用open()開檔。注意open()第二參數除了O_RDONLY以外還有O_NONBLOCK,這是把檔案開啟成非阻擋模式。如果沒有這個flag,目前沒有資料的時候read()會讓程式停住,等有資料的時候再往下走,但本篇我們想要的是即使沒在操作手把程式也要繼續執行。

開檔失敗時open()會傳回-1。鍵盤與滑鼠輸入那一篇有提到鍵盤與滑鼠的device檔通常需要root權限才能讀取,但手把的device檔一般使用者也可讀取,雖然本篇假設open()可能傳回-1,但一般不會遇到開檔失敗的情況。

開啟檔案之後用ioctl()取得一些資訊。
ioctl(2) - Linux man page
ioctl(3) - Linux man page
ioctl是input/output control的縮寫,宣告是這樣
int ioctl(int d, int request, ...);
是C語言的可變參數函式,第一參數是file descriptor,第二參數是要做何種操作,之後的參數依操作而異。上面說到各種硬體裝置在Linux裡會有檔案對應,此函式的用途是控制裝置檔案,能用的操作依裝置種類而異。

手把能用的操作參見上面說的joystick.h和kernel文件,本篇用到的有這些:
常數名稱 第三參數
JSIOCGNAME(len) 0x80006a13+len×0x10000 char[len]
JSIOCGBUTTONS 0x80016a12 char*
JSIOCGAXES 0x80016a11 char*
第三參數是傳回值,從常數名稱可看出傳回的是什麼。JSIOCGNAME第三參數要給一個char陣列;而JSIOCGBUTTONS和JSIOCGAXES傳回的是一個char,宣告一個char變數再把它的指標傳入。
JSIOCGNAME()要在括號內填一個整數代表char陣列的長度,這個macro會代換成一個整數,函式從數值得知第三參數的陣列有多大。
二、讀取手把輸入
用read()讀取剛剛開啟的檔案。read()第二參數是讀取的資料要存在此處,第三參數是這塊空間的byte數。資料的格式是struct js_event,所以第二參數是宣告一個js_event變數將其指標傳入,第三參數填struct js_event的byte數。翻joystick.h可以查到js_event的定義:
struct js_event {
  __u32 time;   /* event timestamp in milliseconds */
  __s16 value;  /* value */
  __u8 type;    /* event type */
  __u8 number;  /* axis/button number */
};
讀取後首先檢查type,它有四種情況。開啟檔案後read()會先讀到一連串type=JS_EVENT_AXIS|JS_EVENT_INIT ,或JS_EVENT_BUTTON|JS_EVENT_INIT的資料,代表初始值,之後手把的按鈕或軸有變化時read()才會讀到資料,JS_EVENT_AXIS和JS_EVENT_BUTTON分別是軸和按鈕。本篇沒有分別初值和輸入,一律用printf()印出數值。

再來是number,代表第幾個軸或按鈕。最後是value,按鈕的話這個值只有1和0,分別是按下和放開,軸是-32767~32767。

剩下一個欄位time是從「某個時候」開始經過的毫秒數,「某個時候」是何時是無法確定的(不是電腦開機以後經過的時間),所以不能作為絕對時間,只能把兩次的數值相減求出相對時間。

用while迴圈反覆讀取輸入直到read()傳回-1,這代表沒有資料了或發生error,檢查全域變數errno判斷原因,如果是沒有資料就暫停20ms後再執行一次。

如果在暫停的20ms之間操作手把,作業系統會把資料暫存,等程式呼叫read()的時候再傳給程式。如果很久沒有呼叫read(),暫存資料會不斷累積導致程式異常。本篇每隔20ms就讀一次不會遇到這個問題,但寫串流式IO的程式有時要考慮這個問題。

Linux處理手把跟鍵盤滑鼠一樣是事件驅動,有變化的時候程式才收到資料,如果想取得目前所有按鈕和軸的狀態,必須自己用變數記錄狀態,每次收到事件時更新狀態。

用這個指令build
gcc joystick.c -o joystick -Os -s
本程式只用到kernel和C語言標準的函式,不用連結任何library。

其他文章有提過,在Linux用printf()印出訊息,要用命令列執行才看得到,在GUI按滑鼠執行是看不到的。

執行畫面,左:初始值,右:操作手把後的反應。


如果在視窗程式用本篇的方法會有個現象:視窗非作用中的時候也會收到手把輸入。如果希望視窗非作用時不要有反應,要用FocusIn和FocusOut這兩個X Window訊息,以後有機會再介紹。



手把輸入有很多細節說明文件也沒寫,是筆者用過很多個手把實測累積的經驗。

每個手把按鈕配置不一樣、十字鈕內部是當成軸處理、藍牙手把斷線後20秒程式才偵測得到、還有偵測手把拔掉重插,這些細節跟Windows篇一樣,可以看Windows篇後半部。
讀取手把輸入 (Windows)

用glob()搜尋檔案是稍微費時的操作。檔案是比記憶體慢的儲存設備,雖然只呼叫glob()時看不出明顯延遲,但遊戲程式每個frame都要做大量計算,時間能省就儘量省,最好分開一個thread搜尋手把,等找到手把再由主執行緒讀取手把訊息。



除了本篇介紹的/dev/input/js*以外,Linux好像只有一個方法讀取手把:讀取/dev/input/event*,這個方法比較新,可以支援force feedback(震動之類的功能),但是js*的方法比較簡易,所以本篇還是介紹js*。
event*把所有類型的輸入輸出,包括電源鈕、聲音輸出都包含進來,要讀取這個檔案得知各個event*是什麼裝置:/proc/bus/input/devices。event*的方法相關常數定義在/usr/include/linux/input-event-codes.h

翻input-event-codes.h可以得知event*的方法把一個視覺頭盔當成兩個軸處理,可以找到ABS_HAT0X、ABS_HAT0Y等常數,且除了按鈕和軸以外支援第三種輸入:相對軸,裡面有REL_X、REL_Y等常數。

至於Linux最多支援幾個按鈕和軸,我沒有找到函式可以取得系統內部的資料,但有找到這一篇,說最多支援80個按鈕。
https://x-plane.vip/2020/07/11/kerneljoystickmax/
軸的話,仿照這篇的方法查input-event-codes.h的常數,查ABS_開頭的最小和最大值。
ABS_X   0x00
ABS_MAX 0x3f

0x00~0x3f是64個軸。

X Window好像不能讀取手把輸入,查了很久也沒有找到。是有一個擴充:xf86-input-joystick,但這是用手把模擬鍵盤和滑鼠訊息,不是讀取手把輸入。
https://www.x.org/releases/current/doc/man/man4/joystick.4.xhtml
https://people.freedesktop.org/~saschahlusiak/xf86-input-joystick.pdf

創作回應

更多創作