前篇:讀取鍵盤與滑鼠輸入 (X Window)
本篇介紹手把輸入。
程式教學目錄
本篇的程式沒有透過X Window,而是從Linux底層讀取,因為X Window沒有讀取手把輸入的功能。
在寫程式之前,先看看作業系統內部的手把訊息是什麼。
各種硬體設備在Linux裡會有檔案對應,這些檔案不是儲存裝置上的資料(磁碟、flash記憶體等等),而是系統自動產生的虛擬檔案,讀寫檔案就是把資料從裝置輸入輸出。輸入裝置對應的檔案放在/dev/input/資料夾內。
把手把的線插進USB孔或設定好藍牙連接後,打這個指令,或用GUI的檔案瀏覽器開這個資料夾看有沒有js開頭的檔案:
連接兩個手把則會出現兩個檔案,沒出現就表示作業系統沒偵測到手把。
打這個指令,如果剛剛找到的檔案不是js0就自己改一下指令。
筆者寫這篇時用的Fedora 33是剛灌好就有內建jstest,如果您使用的發行版沒有內建,自己找一下套件。
之後按按鈕或動類比桿,就會顯示手把的狀態。
打jstest不加參數會顯示這個指令的其他用法,有興趣自己試試。
也有一些圖形介面工具可以看手把輸入,例如jstest-gtk。
或KDE桌面的「系統設定→輸入裝置→遊戲控制器」。
Linux的手把輸入有兩種資料
這是Linux版的snes9x按鈕設定
按十字鈕為何這裡顯示的是Axis而不是button?要看內部資料才能解答。
右邊畫面的Joystick Axis Threshold是把類比桿當成按鈕時,傾斜多少程度才判定是按下按鈕。
這次不需要建視窗,寫個命令列程式就行了。
joystick.c
用無窮迴圈每隔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
一、尋找手把
用這個指令build
本程式只用到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
本篇介紹手把輸入。
程式教學目錄
本篇的程式沒有透過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的手把輸入有兩種資料
- 按鈕(Button)
每個按鈕都是一個on、off的開關,1 bit就可以描述。
有些手把的類比桿可以按下,這也算是按鈕。 - 軸(Axis)
十字鈕與類比桿屬於這種。值是-32767~32767,代表推類比桿的程度,中間位置是0,最左或最上是-32767,最右、下是32767。
十字鈕雖然沒有力度控制但也是當成軸處理,數值只有-32767、0、32767三種情況。
有的手把有類比按鈕(例如Xbox手把),按得淺或深效果不一樣,也是視為軸處理。
這是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的縮寫,宣告是這樣是C語言的可變參數函式,第一參數是file descriptor,第二參數是要做何種操作,之後的參數依操作而異。上面說到各種硬體裝置在Linux裡會有檔案對應,此函式的用途是控制裝置檔案,能用的操作依裝置種類而異。
int ioctl(int d, int request, ...);
手把能用的操作參見上面說的joystick.h和kernel文件,本篇用到的有這些:第三參數是傳回值,從常數名稱可看出傳回的是什麼。JSIOCGNAME第三參數要給一個char陣列;而JSIOCGBUTTONS和JSIOCGAXES傳回的是一個char,宣告一個char變數再把它的指標傳入。
常數名稱 值 第三參數 JSIOCGNAME(len) 0x80006a13+len×0x10000 char[len] JSIOCGBUTTONS 0x80016a12 char* JSIOCGAXES 0x80016a11 char*
JSIOCGNAME()要在括號內填一個整數代表char陣列的長度,這個macro會代換成一個整數,函式從數值得知第三參數的陣列有多大。
用read()讀取剛剛開啟的檔案。read()第二參數是讀取的資料要存在此處,第三參數是這塊空間的byte數。資料的格式是struct js_event,所以第二參數是宣告一個js_event變數將其指標傳入,第三參數填struct js_event的byte數。翻joystick.h可以查到js_event的定義:讀取後首先檢查type,它有四種情況。開啟檔案後read()會先讀到一連串type=JS_EVENT_AXIS|JS_EVENT_INIT ,或JS_EVENT_BUTTON|JS_EVENT_INIT的資料,代表初始值,之後手把的按鈕或軸有變化時read()才會讀到資料,JS_EVENT_AXIS和JS_EVENT_BUTTON分別是軸和按鈕。本篇沒有分別初值和輸入,一律用printf()印出數值。
struct js_event {
__u32 time; /* event timestamp in milliseconds */
__s16 value; /* value */
__u8 type; /* event type */
__u8 number; /* axis/button number */
};
再來是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 |
其他文章有提過,在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