Implementando um teclado virtual no Linux

- por Sergio Prado

Categorias: Linguagem C, Linux embarcado Tags: , ,

A idéia de escr­ever este artigo surgiu da neces­si­dade de um pro­jeto que tra­bal­hei algu­mas sem­anas atrás.

O obje­tivo era imple­men­tar um teclado vir­tual, de forma que um processo ou apli­cação pudesse sim­u­lar o pres­sion­a­mento de uma tecla, sem que esta tecla tivesse sido real­mente pres­sion­ada em um teclado físico.

Até aí tudo bem, qual­quer bib­lioteca ou toolkit grá­fico decente (X11, DirectFB, Qt, etc) pos­sui algum mecan­ismo para emu­lar entrada de teclado. Mas o obje­tivo era ser inde­pen­dente de bib­lioteca grá­fica, e fun­cionar tam­bém em ambi­ente somente texto. Ou seja, pre­cisava­mos de algo imple­men­tado den­tro do ker­nel.

Mas não vamos “colo­car a car­roça na frente dos bois”! Antes de pen­sar na solução, vamos pen­sar no prob­lema…

PURA MÁGICA?

Não, não é mág­ica. Mas uma tecla pres­sion­ada den­tro do ker­nel do Linux passa por algu­mas camadas até você vê-la ecoando na tela do seu mon­i­tor.

CAMADA 0: HARDWARE

Tudo começa no hard­ware, claro. A con­tro­ladora do teclado conec­tado ao seu PC varre e decod­i­fica a matriz de teclas e cuida de detal­hes como o con­t­role de debounc­ing.

Quando você pres­siona uma tecla, essa tecla é trans­for­mada em um código chamado de scan­code. Cada tecla pos­sui um scan­code quando é pres­sion­ada e outro quando é lib­er­ada (o bit mais sig­ni­fica­tivo é setado). Por exem­plo, a letra ‘x’ emite o scan­code 0x2d quando é pres­sion­ada e 0xad quando é lib­er­ada (no meu teclado USB).

Se você quiser, pode usar a fer­ra­menta “showkey” para fazer o dump e exibir o scan­code das teclas pres­sion­adas no seu PC:

$ sudo showkey -s
press any key (program terminates 10s after last keypress)...
0x2d 0xad

Por­tanto, para cada tecla pres­sion­ada, uma inter­rupção é ger­ada para a CPU, e os scan­codes são envi­a­dos via bar­ra­mento de comu­ni­cação, depen­dendo do hard­ware do seu teclado (PS2, USB, etc).

Logo após, a rotina de trata­mento de inter­rupção do teclado é acionada, sendo respon­sável por rece­ber e tratar estes scan­codes, con­forme ver­e­mos a seguir.

CAMADA 1: DEVICE DRIVER

Aqui já esta­mos no ker­nel do Linux. Um device dri­ver vai con­ver­sar com o hard­ware do seu teclado, rece­ber e tratar os scan­codes.

E depen­dendo do hard­ware que você esta usando (teclado USB, PS2, etc), um difer­ente device dri­ver será o respon­sável por ler estes scan­codes. Por exem­plo, a imple­men­tação do teclado PS2 padrão encontra-se nos fontes do ker­nel em drivers/input/keyboard/atkbd.c. Vai lá dar uma olhada, eu espero!

Após ler os scan­codes, o device dri­ver irá convertê-los em um outro código chamado key­codes. Cuidado para não con­fundir! Scan­code é um código depen­dente do hard­ware do teclado e tratado pelo device dri­ver. Key­code é um código que rep­re­senta uma tecla den­tro de um sis­tema Linux.

Você já viu que cada tecla pres­sion­ada gera dois scan­codes (pres­sion­ada e lib­er­ada). Mas ela pos­sui um único key­code. Por exem­plo, no meu PC, quando pres­siono a tecla ‘x’, é ger­ado o key­code 45:

$ sudo showkey -k
press any key (program terminates 10s after last keypress)...
keycode  45 press
keycode  45 release

Mas como então o ker­nel difer­en­cia teclas pres­sion­adas e lib­er­adas se o key­code é o mesmo? Fácil. O device dri­ver gera dois even­tos para o ker­nel. Um para teclas pres­sion­adas e outro para teclas lib­er­adas. E estes even­tos são envi­a­dos para camada input do ker­nel.

CAMADA 2: INPUT SUBSYSTEM

É nesta camada que percebe­mos toda a capaci­dade de mod­u­lar­iza­ção do ker­nel do Linux. 

A camada input foi cri­ada para abstrair a cap­tura de even­tos de dis­pos­i­tivos de entrada como mouse, teclado, joy­sticks, touch screens, etc. Enquanto que cada um destes dis­pos­i­tivos pos­sui seu respec­tivo device dri­ver, cada device dri­ver exporta os even­tos para a camada input. Por sua vez, a camada input trata e exporta estes even­tos em um for­mato padrão para arquivos de dis­pos­i­tivo em /dev/input/:

$ ls /dev/input
event0  event1  event2  event3  event4  event5  mice  mouse0

Cada um destes arquivos de dis­pos­i­tivo rep­re­sen­tam um dis­pos­i­tivo de entrada.

Você pode usar a fer­ra­menta “evtest” para ver­i­ficar qual o dis­pos­i­tivo rela­cionado à deter­mi­nado arquivo:

$ sudo evtest /dev/input/event4
Input driver version is 1.0.1
Input device ID: bus 0x3 vendor 0x1c4f product 0x2 version 0x110
Input device name: "USB USB Keykoard"
...

Veja que, na minha máquina, /dev/input/event4 é o arquivo de dis­pos­i­tivo que gera os even­tos do teclado USB.

A fer­ra­menta “evtest” tam­bém é capaz de mon­i­torar os even­tos de dis­pos­i­tivos de entrada. Exe­cute nova­mente o comando acima, pres­sione uma tecla e veja os even­tos que foram expor­ta­dos para user­space através deste arquivo de dis­pos­i­tivo:

...
Event: time 1323720735.849223, type 1 (Key), code 45 (X), value 1
Event: time 1323720735.849225, -------------- Report Sync ------------
Event: time 1323720735.905222, type 1 (Key), code 45 (X), value 0
Event: time 1323720776.880210, -------------- Report Sync ------------
...

Perceba o key­code 45 para a tecla ‘x’ e os val­ores 1 para tecla pres­sion­ada e 0 para tecla lib­er­ada.

Por fim, as bib­liote­cas grá­fi­cas mon­i­toram os even­tos nestes arquivos de dis­pos­i­tivos e expor­tam estes even­tos para as apli­cações. E assim você vê uma tecla ecoando em seu ter­mi­nal!

Linux é ou não é um dos SOMLTT — sis­temas opera­cionais mais legais de todos os tem­pos? :-)

Ape­nas um detalhe aqui: no caso do teclado, além de expor­tar os even­tos para “/dev/input/”, a camada input tam­bém exporta os even­tos de teclas pres­sion­adas para a camada TTY ativa no momento. Desta forma, se você estiver em um ter­mi­nal conec­tado à /dev/tty2, por exem­plo, irá rece­ber este evento. Se quiser saber mais sobre a camada TTY, leia o artigo “Por den­tro da con­sole em sis­temas Linux”.

VOLTANDO AO NOSSO PROBLEMA

Depois de todas estas expli­cações, espero que você não tenha se esque­cido do nosso prob­lema orig­i­nal: emu­lar um teclado vir­tual inde­pen­dente de bib­lioteca grá­fica, e que fun­cione em modo texto.

Agora ficou mais fácil pen­sar numa solução, não é verdade?

Reveja as camadas. A camada 0 é hard­ware, e não temos hard­ware no nosso caso. A camada 2 é genérica, e não é uma boa prática mexer nela. Potanto, é na camada 1 que tra­bal­hare­mos.

Vamos desen­volver um device dri­ver (no nosso caso, um módulo do ker­nel, já que não falare­mos com nen­hum hard­ware) que irá gerar even­tos de teclado para a camada input. Sim­ples assim!

A SOLUÇÃO

Para não com­plicar muito, vamos escr­ever um mod­ulo sim­ples que irá emu­lar a dig­i­tação de uma frase a cada 10 segun­dos. Assim que car­regado, uma frase será “dig­i­tada” pelo módulo a cada 10 segun­dos. E para deixar o tra­balho mais diver­tido, a frase será “seg­men­ta­tion fault”!

Sem mais enro­lação, este é o código com­pleto do nosso módulo do ker­nel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include "linux/fs.h"
#include "linux/cdev.h"
#include "linux/module.h"
#include "linux/kernel.h"
#include "linux/delay.h"
#include "linux/kthread.h"
#include "linux/device.h"
#include "linux/slab.h"
#include "linux/tty.h"
#include "linux/tty_flip.h"
#include "linux/kbd_kern.h"
#include "linux/input.h"
 
/* vtkbd kernel thread struct */
static struct task_struct *vtkbd_thread_task;
 
/* vtkbd input device structure */
static struct input_dev *vtkbd_input_dev;
 
const char str_keys[] = { KEY_S, KEY_E, KEY_G, KEY_M, KEY_E, KEY_N,
                          KEY_T, KEY_A, KEY_T, KEY_I, KEY_O, KEY_N,
                          KEY_SPACE, KEY_F, KEY_A, KEY_U, KEY_L,
                          KEY_T, KEY_ENTER };
 
/* kernel thread */
static int vtkbd_thread(void *unused)
{
    int i;
 
    while (!kthread_should_stop()) {
 
        for (i = 0; i < sizeof(str_keys); i++) {
 
            input_report_key(vtkbd_input_dev, str_keys[i], 1);
            input_report_key(vtkbd_input_dev, str_keys[i], 0);
            input_sync(vtkbd_input_dev);
 
        }
 
        /* wait 10 seconds */
        msleep(10000);
    }
 
    return(0);
}
 
/* driver initialization */
static int __init vtkbd_init(void)
{
    static const char *name = "Virtual Keyboard";
    int i;
 
    /* allocate input device */
    vtkbd_input_dev = input_allocate_device();
    if (!vtkbd_input_dev) {
        printk("vtkbd_init: Error on input_allocate_device!\n");
        return -ENOMEM;
    }
 
    /* set input device name */
    vtkbd_input_dev->name = name;
 
    /* enable key events */
    set_bit(EV_KEY, vtkbd_input_dev->evbit);
    for (i = 0; i < 256; i++)
        set_bit(i, vtkbd_input_dev->keybit);
 
    /* register input device */
    input_register_device(vtkbd_input_dev);
 
    /* start thread */
    vtkbd_thread_task = kthread_run(vtkbd_thread, NULL, "%s", "vtkbd_thread");
 
    printk("Virtual Keyboard driver initialized.\n");
 
    return 0;
}
 
/* driver exit */
void __exit vtkbd_exit(void)
{
    /* stop thread */
    kthread_stop(vtkbd_thread_task);
 
    /* unregister input device */
    input_unregister_device(vtkbd_input_dev);
 
    printk("Virtual Keyboard driver.\n");
}
 
module_init(vtkbd_init);
module_exit(vtkbd_exit);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sergio Prado sergio.prado@embeddedlabworks.com");
MODULE_DESCRIPTION("Virtual Keyboard driver");

O módulo tem basi­ca­mente 3 funções: vtkbd_init() para ini­cializar o módulo, vtkbd_exit() para fazer a limpeza ao descar­regar o módulo e vtkbd_thread() que faz a mág­ica de dig­i­tar a frase periodicamente.

Na função de ini­cial­iza­ção, o primeiro passo é alo­car um dis­pos­i­tivo de input com a função input_allocate_device() na linha 54. Essa função vai devolver uma estru­tura que vai nos pos­si­bil­i­tar con­ver­sar com a camada input e gerar os even­tos de teclado. Nas lin­has 64 a 66 habili­ta­mos a ger­ação de even­tos em todas as teclas (veja o loop) e na linha 69 reg­is­tramos o dis­pos­i­tivo de input com a função input_register_device(). Por último, cri­amos e ini­ci­amos a thread do ker­nel que irá fazer o “tra­balho sujo”. Se você quiser ler mais sobre Ker­nel Threads, leia o artigo “Linux Device Dri­vers — Tra­bal­hando com Ker­nel Threads

Na função de limpeza, paramos a thread e remove­mos o dis­pos­i­tivo de input que reg­is­tramos na inicialização.

A thread vtkbd_thread() é bem sim­ples e faz toda a mág­ica. Ela é basi­ca­mente um loop infinito que, a cada 10 segun­dos, gera os even­tos para emu­lar a dig­i­tação. O vetor str_keys[] con­tém os key­codes para a frase “seg­men­ta­tion fault”. Esses key­codes estão definidos em include/linux/input.h. Para cada key­code, ger­amos dois even­tos com a função input_report_key(), sim­u­lando a tecla pres­sion­ada na linha 34 e a tecla lib­er­ada na linha 35 (veja os parâmet­ros 1 e 0, respec­ti­va­mente). Por último, exe­cu­ta­mos a função input_sync(), noti­f­i­cando a camada input da existên­cia de novos even­tos a serem tratados.

Sim­ples, não?

Para com­pi­lar, crie um Make­file com o con­teúdo abaixo:

KVER := $(shell uname -r)
KDIR := /usr/src/linux-headers-$(KVER)
PWD  := $(shell pwd)
 
obj-m += vtkbd.o
 
default:
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
 
clean:
        @rm -Rf *.o *.ko *.mod.c modules.order Module.symvers Module.markers

E com­pile:

$ make

Para car­regar o módulo:

$ sudo insmod vtkbd.ko

E para remover o módulo:

$ sudo rmmod vtkbd

Como o módulo ficará dig­i­tando “seg­men­ta­tion fault” a cada 10 segun­dos, este é o tempo que você terá para dig­i­tar o comando de remoção do módulo antes dele bagunçar seu shell!

E lembre-se de que você esta tra­bal­hando em ker­nel space, com acesso total e irrestrito à memória e I/Os. Por­tanto, o ideal aqui é tra­bal­har em uma máquina vir­tual. É mais seguro e pode evi­tar muitos aci­dentes, prin­ci­pal­mente para aque­les que estão começando os estu­dos do Ker­nel do Linux.

PEGADINHA DO MALANDRO

Agora pegue este módulo, e quando seu amigo deixar a seção aberta para tomar um café, instale e cronome­tre o tempo que ele vai levar para encon­trar a causa do “seg­men­ta­tion fault”! Isso se ele encon­trar… :)

Um abraço,

Ser­gio Prado

  • Thi­ago Coutinho

    Muito inter­es­sante, obri­gado por compartilhar!

  • Buli Nan­cio

    A matéria é exe­lente. Testei aqui e deu certo. Quando insiro o módulo tem a seguinte saida:
    ————
    $ seg­men­ta­tion fault
    bash: seg­men­ta­tion: com­mand not found
    $
    ————–
    Ou seja, a shell inter­preta a saida como se fosse um comando. Isso é o com­por­ta­mento esperado?

    • http://sergioprado.org Ser­gio Prado

      É isso aí Buli. É como se tivesse um “fan­tasma” no seu teclado dig­i­tando “seg­men­ta­tion fault” e pres­sio­n­ando ENTER!

      Um abraço.

  • Arnaldo

    Exce­lente artigo!

  • Júlio Hof­fi­mann Mendes

    Oi Sér­gio,
    Como  sem­pre arti­gos inter­es­santes!
    Abraço!

  • Dou­glas

    Parabéns pelo artigo, man­dou bem…

  • Saulo

    Muito bom o con­teúdo do post!
    Obri­gado por compartilhar!

  • http://luizlmarins.wordpress.com lui­zl­marins

    Ser­gio,
    Na prática, para um usuário final desk­top, em que situ­ações pode­ria ser-lhe útil?

    • http://sergioprado.org Ser­gio Prado

      Olá Luiz,

      Este exem­plo não teria nen­huma util­i­dade. Mas em apli­cações Desk­top pode­ria ser útil se você pre­cisasse desen­volver uma apli­cação que exibisse na tela um teclado virtual.

      Um abraço.

  • fb

    valeu pela dica, eu tava pre­cisando de um esquema desse, pra ficar pres­sio­n­ando uma mesma tecla a cada segundo enquanto o mod­ulo estiver ativo, porem ainda nao con­segui fazer rodar no ubuntu, primeiro dava erro no make­file, mudei o make­file e aí deu erro de depen­den­cias mesmo com o linux-headers insta­l­ado. Instalei o fonte com­pleto aí com­pi­lou nor­mal, porem na hora de car­regar o mod­ulo com o ins­mod dá erro, mas estou vendo o q posso fazer pra resolver.

    Uma duvida: pre­ciso fazer um esquema mas nao sei se é pos­sivel: den­tro de um pro­grama em C incluir um sys­tem(“”) pra exe­cu­tar um comando e em caso de retorno pos­i­tivo do shell, exe­cu­tar o pres­sion­a­mento de uma tecla uma vez ape­nas quando acon­te­cer o evento onde o sys­tem foi inserido.
    Pen­sei em fazer um mod­ulo que quando é car­regado digita uma letra auto­matico, aí no sys­tem eu colo­caria ins­mod pra car­regar o mod­ulo e depois outro sys­tem pra remover o mod­ulo com rmmod.

  • Lucas De Marchi

    você pode­ria usar o uin­put e deixar o processo em user space ao invés de uma thread do ker­nel. A gente faz isso no BlueZ para enviar para o sis­tema con­troles rece­bidos via AVRCP. Quando você abre o device /dev/uinput e chama ioctl(fd, UI_DEV_CREATE, NULL) ele vai criar um novo dis­pos­i­tivo de entrada. Tem um exem­plo bem fácil no seguinte link: http://thiemonge.org/getting-started-with-uinput

    • http://sergioprado.org/ Ser­gio Prado

      Olá Lucas!

      Obri­gado pela dica. Não con­hecia o uinput.

      By the way, parabéns pelo tra­balho com o kmod!

      Um abraço.

  • Eduardo Pin­heiro

    Viva Sér­gio,

    Exce­lente artigo.

    Será que me pode aju­dar a perce­ber como faço um vir­tual key­board (usando este exem­plo) mas para desk­tops vir­tu­ais difer­entes (um módulo destes para cada Desktop)?

    Ex:
    No desk­top :0 => quero que esteja sem­pre a escr­ever “Seg­men­ta­tion Fault to desk­top 0″
    No desk­top :1 => quero que esteja sem­pre a escr­ever “Seg­men­ta­tion Fault to desk­top 1″
    No desk­top :2 => quero que esteja sem­pre a escr­ever “Seg­men­ta­tion Fault to desk­top 2″
    etc, etc

    Obri­gado,

    Eduardo

    • http://sergioprado.org/ Ser­gio Prado

      Olá Eduardo,

      O que você quer dizer com desk­tops vir­tu­ais? Diver­sos sis­temas rodando em máquinas vir­tu­ais diferentes?

      • Eduardo Pin­heiro

        Viva Sér­gio. Várias instân­cias de X a cor­rer no mesmo com­puta­dor. ex: startx — :0 ; startx — :1; startx — :2

        • http://sergioprado.org/ Ser­gio Prado

          Olá Eduardo,

          Acred­ito que daria para car­regar um módulo para cada instan­cia do X. E então con­fig­u­rar o X para “escu­tar” ape­nas o vir­tual key­board que você deseja.

          Outra opção é usar o uin­put sug­erido pelo Lucas mais abaixo.

          Um abraço.

          • Eduardo Pin­heiro

            Olá Sér­gio. É isso que estaria a pen­sar tam­bém. Quanto ao uin­put averiguar. Obrigado

Navegue
Licença Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.