Implementando um teclado virtual no Linux
- por Sergio Prado
A idéia de escrever este artigo surgiu da necessidade de um projeto que trabalhei algumas semanas atrás.
O objetivo era implementar um teclado virtual, de forma que um processo ou aplicação pudesse simular o pressionamento de uma tecla, sem que esta tecla tivesse sido realmente pressionada em um teclado físico.
Até aí tudo bem, qualquer biblioteca ou toolkit gráfico decente (X11, DirectFB, Qt, etc) possui algum mecanismo para emular entrada de teclado. Mas o objetivo era ser independente de biblioteca gráfica, e funcionar também em ambiente somente texto. Ou seja, precisavamos de algo implementado dentro do kernel.
Mas não vamos "colocar a carroça na frente dos bois"! Antes de pensar na solução, vamos pensar no problema…
PURA MÁGICA?
Não, não é mágica. Mas uma tecla pressionada dentro do kernel do Linux passa por algumas camadas até você vê-la ecoando na tela do seu monitor.
CAMADA 0: HARDWARE
Tudo começa no hardware, claro. A controladora do teclado conectado ao seu PC varre e decodifica a matriz de teclas e cuida de detalhes como o controle de debouncing.
Quando você pressiona uma tecla, essa tecla é transformada em um código chamado de scancode. Cada tecla possui um scancode quando é pressionada e outro quando é liberada (o bit mais significativo é setado). Por exemplo, a letra 'x' emite o scancode 0x2d quando é pressionada e 0xad quando é liberada (no meu teclado USB).
Se você quiser, pode usar a ferramenta "showkey" para fazer o dump e exibir o scancode das teclas pressionadas no seu PC:
$ sudo showkey -s press any key (program terminates 10s after last keypress)... 0x2d 0xad |
Portanto, para cada tecla pressionada, uma interrupção é gerada para a CPU, e os scancodes são enviados via barramento de comunicação, dependendo do hardware do seu teclado (PS2, USB, etc).
Logo após, a rotina de tratamento de interrupção do teclado é acionada, sendo responsável por receber e tratar estes scancodes, conforme veremos a seguir.
CAMADA 1: DEVICE DRIVER
Aqui já estamos no kernel do Linux. Um device driver vai conversar com o hardware do seu teclado, receber e tratar os scancodes.
E dependendo do hardware que você esta usando (teclado USB, PS2, etc), um diferente device driver será o responsável por ler estes scancodes. Por exemplo, a implementação do teclado PS2 padrão encontra-se nos fontes do kernel em drivers/input/keyboard/atkbd.c. Vai lá dar uma olhada, eu espero!
Após ler os scancodes, o device driver irá convertê-los em um outro código chamado keycodes. Cuidado para não confundir! Scancode é um código dependente do hardware do teclado e tratado pelo device driver. Keycode é um código que representa uma tecla dentro de um sistema Linux.
Você já viu que cada tecla pressionada gera dois scancodes (pressionada e liberada). Mas ela possui um único keycode. Por exemplo, no meu PC, quando pressiono a tecla 'x', é gerado o keycode 45:
$ sudo showkey -k press any key (program terminates 10s after last keypress)... keycode 45 press keycode 45 release |
Mas como então o kernel diferencia teclas pressionadas e liberadas se o keycode é o mesmo? Fácil. O device driver gera dois eventos para o kernel. Um para teclas pressionadas e outro para teclas liberadas. E estes eventos são enviados para camada input do kernel.
CAMADA 2: INPUT SUBSYSTEM
É nesta camada que percebemos toda a capacidade de modularização do kernel do Linux.
A camada input foi criada para abstrair a captura de eventos de dispositivos de entrada como mouse, teclado, joysticks, touch screens, etc. Enquanto que cada um destes dispositivos possui seu respectivo device driver, cada device driver exporta os eventos para a camada input. Por sua vez, a camada input trata e exporta estes eventos em um formato padrão para arquivos de dispositivo em /dev/input/:
$ ls /dev/input event0 event1 event2 event3 event4 event5 mice mouse0 |
Cada um destes arquivos de dispositivo representam um dispositivo de entrada.
Você pode usar a ferramenta "evtest" para verificar qual o dispositivo relacionado à determinado 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 dispositivo que gera os eventos do teclado USB.
A ferramenta "evtest" também é capaz de monitorar os eventos de dispositivos de entrada. Execute novamente o comando acima, pressione uma tecla e veja os eventos que foram exportados para userspace através deste arquivo de dispositivo:
... 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 keycode 45 para a tecla 'x' e os valores 1 para tecla pressionada e 0 para tecla liberada.
Por fim, as bibliotecas gráficas monitoram os eventos nestes arquivos de dispositivos e exportam estes eventos para as aplicações. E assim você vê uma tecla ecoando em seu terminal!
Linux é ou não é um dos SOMLTT – sistemas operacionais mais legais de todos os tempos? :-)
Apenas um detalhe aqui: no caso do teclado, além de exportar os eventos para "/dev/input/", a camada input também exporta os eventos de teclas pressionadas para a camada TTY ativa no momento. Desta forma, se você estiver em um terminal conectado à /dev/tty2, por exemplo, irá receber este evento. Se quiser saber mais sobre a camada TTY, leia o artigo "Por dentro da console em sistemas Linux".
VOLTANDO AO NOSSO PROBLEMA…
Depois de todas estas explicações, espero que você não tenha se esquecido do nosso problema original: emular um teclado virtual independente de biblioteca gráfica, e que funcione em modo texto.
Agora ficou mais fácil pensar numa solução, não é verdade?
Reveja as camadas. A camada 0 é hardware, e não temos hardware no nosso caso. A camada 2 é genérica, e não é uma boa prática mexer nela. Potanto, é na camada 1 que trabalharemos.
Vamos desenvolver um device driver (no nosso caso, um módulo do kernel, já que não falaremos com nenhum hardware) que irá gerar eventos de teclado para a camada input. Simples assim!
A SOLUÇÃO
Para não complicar muito, vamos escrever um modulo simples que irá emular a digitação de uma frase a cada 10 segundos. Assim que carregado, uma frase será "digitada" pelo módulo a cada 10 segundos. E para deixar o trabalho mais divertido, a frase será "segmentation fault"!
Sem mais enrolação, este é o código completo do nosso módulo do kernel:
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 basicamente 3 funções: vtkbd_init() para inicializar o módulo, vtkbd_exit() para fazer a limpeza ao descarregar o módulo e vtkbd_thread() que faz a mágica de digitar a frase periodicamente.
Na função de inicialização, o primeiro passo é alocar um dispositivo de input com a função input_allocate_device() na linha 54. Essa função vai devolver uma estrutura que vai nos possibilitar conversar com a camada input e gerar os eventos de teclado. Nas linhas 64 a 66 habilitamos a geração de eventos em todas as teclas (veja o loop) e na linha 69 registramos o dispositivo de input com a função input_register_device(). Por último, criamos e iniciamos a thread do kernel que irá fazer o "trabalho sujo". Se você quiser ler mais sobre Kernel Threads, leia o artigo "Linux Device Drivers – Trabalhando com Kernel Threads"
Na função de limpeza, paramos a thread e removemos o dispositivo de input que registramos na inicialização.
A thread vtkbd_thread() é bem simples e faz toda a mágica. Ela é basicamente um loop infinito que, a cada 10 segundos, gera os eventos para emular a digitação. O vetor str_keys[] contém os keycodes para a frase "segmentation fault". Esses keycodes estão definidos em include/linux/input.h. Para cada keycode, geramos dois eventos com a função input_report_key(), simulando a tecla pressionada na linha 34 e a tecla liberada na linha 35 (veja os parâmetros 1 e 0, respectivamente). Por último, executamos a função input_sync(), notificando a camada input da existência de novos eventos a serem tratados.
Simples, não?
Para compilar, crie um Makefile com o conteú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 compile:
$ make |
Para carregar o módulo:
$ sudo insmod vtkbd.ko |
E para remover o módulo:
$ sudo rmmod vtkbd |
Como o módulo ficará digitando "segmentation fault" a cada 10 segundos, este é o tempo que você terá para digitar o comando de remoção do módulo antes dele bagunçar seu shell!
E lembre-se de que você esta trabalhando em kernel space, com acesso total e irrestrito à memória e I/Os. Portanto, o ideal aqui é trabalhar em uma máquina virtual. É mais seguro e pode evitar muitos acidentes, principalmente para aqueles que estão começando os estudos do Kernel 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 cronometre o tempo que ele vai levar para encontrar a causa do "segmentation fault"! Isso se ele encontrar… :)
Um abraço,
Sergio Prado