sábado, 21 de setembro de 2019

Controle remoto para Linux utilizando um Arduino

Bateu um tédio, pouco serviço pra fazer, e oque precisa ser feito é muito chato. Resolvi fazer um projeto que estava muito tempo na fila: utilizar um controle remoto para controlar um computador que uso no rádio e tv. Da muita preguiça chato ter ir até o computador para mudar de música, pausar o filme no netflix, ou outra coisa qualquer.

O projeto é simples, utiliza um arduino UNO, um controle remoto que estava sobrando - da propria TV, uma panasonic, e um receptor de IR manjado ( TSOP4838 ). O arduino interpreta o sinal vindo do TSOP4838, comunica pelo USB com o computador, e um programa em python converte isso para comandos em Linux.


Receptor IR - Arduino

O hardware não tem nada demais, é o receptor de IR espetado no arduino. Coloquei tudo dentro de um copo do starbuck para não ficar dando curto com o resto dos equipamentos que estão no rack da tv.

O programa utilizado no Arduino é baseado num dos exemplos disponíves com a biblioteca IRremote. Foi adicionado código para lidar coma alimentação do sensor (pino 12 = GND e pino 13 = VCC), e tudo oque era descenessário foi retirado. No meu caso tive sorte do protocolo do controle remoto que eu tinha ser suportado pela biblioteca. No meu caso o protocolo utilizado é o PANASONIC.

#include <IRremote.h>

int RECV_PIN = 11;
int RECV_VCC_PIN = 13;
int RECV_GND_PIN = 12;

IRrecv irrecv(RECV_PIN);

decode_results results;

void setup()
{
  digitalWrite(RECV_GND_PIN, LOW);
  digitalWrite(RECV_VCC_PIN, HIGH);

  pinMode(RECV_GND_PIN, OUTPUT);
  pinMode(RECV_VCC_PIN, OUTPUT);
  
  Serial.begin(9600);
  irrecv.enableIRIn(); // Start the receiver
}

void loop() {
  if (irrecv.decode(&results)) {
    Serial.println(results.value, HEX);
    irrecv.resume(); // Receive the next value
}

Esse programa retorna um número hexadecimal para cada tecla do controle. Mapeando a função com cada código retornado é feito um mapa do controle remoto. Neste controle da Panasonic algumas teclas mandam o mesmo código 3 vezes, geralmente as teclas que vc não que que fique repetindo, como play/pause e outras. As demais teclas com volume ficam mandando o código periodicamente enquanto o botão é apertado.



Interpretador em python

Programa python para recepção dos dados. Utiliza biblioteca pySerial e a ferramenta xdotool. Utilizo o python3. O programa acabou ficando mais complicado que o previsto inicialmente porque o receptor pega um pouco de ruído e envia alguns códigos incorretos. O software também gera uma movimentação não linear para o ponteiro do mouse, ou seja, começa devagar e vai movendo cada vez mais rápido, isso torna o controle pelo controle remoto mais fluido. O código esta bastante comentado.

# Recepção de control remoto
import time
import serial 
import os

VERSION = "0.1"
SERIAL_PORT = "/dev/ttyACM0"
BAUD_RATE = 9600

REPEATABLE = True
NOT_REPEATABLE = False
UNKNOW_COMMAND = '??'
NO_COMMAND = ''
NO_DESCRIPTION = ''
NO_IR_CODE = -1
#Lista de relacao entre codigo IR e comando linux. Formato [Codigo IR, Comando Linux, Descricao, Pode Repetir?]
command_list = [
 ['E17AF807','amixer -q -D pulse sset Master toggle','mute',NOT_REPEATABLE],
 ['BE3BB37','xdotool key XF86AudioRaiseVolume','aumenta volume',REPEATABLE],
 ['D4D9F2A7','xdotool key XF86AudioLowerVolume','baixa volume',REPEATABLE],
 ['21035431','xdotool mousemove_relative -- %MOUSE_MOVE 0','seta para direita',REPEATABLE],
 ['983AB4C1','xdotool mousemove_relative -- -%MOUSE_MOVE 0','seta para esquerda',REPEATABLE],
 ['C20370A1','xdotool mousemove_relative -- 0 -%MOUSE_MOVE','seta para cima',REPEATABLE],
 ['81930A09','xdotool mousemove_relative -- 0 %MOUSE_MOVE','seta para baixo',REPEATABLE],
 ['BB0ED9E1','xdotool click 1; sleep 0.2','ok',NOT_REPEATABLE],
 ['AA7B2167','xdotool key XF86AudioPlay','play',NOT_REPEATABLE],
 ['2E8C2A89','xdotool key XF86AudioNext','fwd',NOT_REPEATABLE],
 ['1F6E01C9','xdotool key XF86AudioPrev','rew',NOT_REPEATABLE],
 ['1D15F4D7','xdotool key XF86AudioStop','stop',NOT_REPEATABLE],
 ['406A954D','systemctl poweroff -i','desliga',NOT_REPEATABLE],
 ['E17A24DB','xset dpms force off; sleep 1','apaga tv',NOT_REPEATABLE],
 ['E17A18E7','xdotool key C; sleep 1','apaga tv',NOT_REPEATABLE],
 ['E17A7887','xdotool getwindowfocus getwindowname;sleep 1','pega nome janela em foco',NOT_REPEATABLE],
 ['A26409C9','pkill --oldest chrome; google-chrome-stable deezer.com/br/ & sleep 1','Abre pagina deezer',NOT_REPEATABLE],
 ['240C9161','pkill --oldest chrome; google-chrome-stable www.netflix.com & sleep 1','Abre pagina netflix',NOT_REPEATABLE],
 ['68E839F1','pkill --oldest chrome; google-chrome-stable www.youtube.com & sleep 1','Abre pagina youtube',NOT_REPEATABLE],
 ['ABBE1086','xdotool key Escape','esc',NOT_REPEATABLE], 
 ['68E69B7F','xdotool click 4','scrool up',REPEATABLE],
 ['223C02A7','xdotool click 5','scrool down',REPEATABLE]
]

#Codigo q corresponde a tecla repetida do IR. Tecla ainda esta apertada
button_down = 'FFFFFFFF'
#inicializa para evitar possivel chamada dessa variavel sem inicilizacao
linux_command = NO_COMMAND
last_command = NO_COMMAND
description = NO_DESCRIPTION
allow_repeat = NOT_REPEATABLE
repetition_count=0
last_ir_code = NO_IR_CODE
ir_code = NO_IR_CODE
#tempo max para interpretar que a tecla foi solta
IR_TIMEOUT = 0.3
#sinaliza q tecla esta pressionada desde ultima vez
pressed = False
#Maximo deslocamento do mouse
max_mouse = 35

# Hello message
print("Controle Remoto Arduino/Linux TabaJairo para controle Panasonic")
print("Versao: " + VERSION);


# Tenta conectar a serial
serial_connected = False
print("Procurando porta serial...")
while (not serial_connected):
 try:
  ser = serial.Serial(port=SERIAL_PORT,baudrate=BAUD_RATE, timeout=1)
  serial_connected = True
  print("yeah...achei")
 except:
  print("buuu...ainda não achei")
  time.sleep(1)
  

#Loop infinito - ctrl+c para terminar
while 1 :
 #Recebe ir code. Aguarda enquanto nao tem nada na serial
 last_ir_code = ir_code
 t = 0
 while ser.inWaiting() == 0:
  time.sleep(0.1)
  t = t + 0.1
  if t > IR_TIMEOUT:
   t=0
   #limpa ultimo codigo pois estourou timeout
   last_ir_code = NO_IR_CODE
 out = ser.readline().rstrip()
 ir_code = out.decode('utf-8')


 #Procura comando na lista
 #Coloca NO_COMMAND em linux_command para indicar q nao foi encontrado
 linux_command = NO_COMMAND
 for command in command_list:
  if ir_code == command[0]:
   linux_command = command[1]
   description = command[2]
   allow_repeat = command[3]

 # Se teve timeout na recepcao do codigo IR entao o ultimo comando eh vazio
 if last_ir_code == NO_IR_CODE:
  last_command = NO_COMMAND
  
 pressed = False
 #Verifica se eh comando repetido (tecla pressionada)
 if (linux_command == last_command) and (last_ir_code != NO_IR_CODE):
  pressed = True
  
 # Aqui tem um truque para melhor responsividade: se o comando atual nao eh reconhecido e 
 #  nao teve timeout na recepcap de IR supoe que houve problema na recepcao do codigo:
 #  o codigo eh diferente mas corresponde a ultima tecla pressionada
 if (linux_command == NO_COMMAND) and (last_ir_code != NO_IR_CODE) and (last_command != NO_COMMAND):
  pressed = True
  linux_command = last_command
  print("Codigo IR invalido! Provavelmente eh o ultimo comando, vamos repetir..."); 
 
 # Incrementa contador de botao pressionado
 if pressed:
  repetition_count+=1
 else:
  repetition_count=0

 # Atualiza last command. A posicao da linha eh importante para o codigo funcionar  
 last_command = linux_command
  
 #subsitui wildcard %MOUSE_MOVE do movimento do mouse para fazer aceleracao
 mouse_move = repetition_count*repetition_count+1;
 if mouse_move > max_mouse:
  mouse_move = max_mouse
 linux_command = linux_command.replace("%MOUSE_MOVE",str(mouse_move)) 

 #executa comando se for conhecido. Verifica se ele pode ser repetido ou nao.
 # Nao queremos o play/pause repetindo, nem o mute por exemplo
 if linux_command != NO_COMMAND:
  if repetition_count == 0 or  (repetition_count > 0 and allow_repeat):
   print(ir_code + ' ' +  str(repetition_count) + ' --> ' + description + ' --> ' + linux_command); 
   os.system(linux_command)
  else:
   print(ir_code + ' ' +  str(repetition_count) + ' --> ' + description + ' --> nao pode repetir');
 else:
  print(ir_code + ' ' +  str(repetition_count) + ' --> buuu, nao conheco esse comando ');


O programa monitor a porta serial /dev/ttyACM0 na qual o arduino esta conectada. Sempre que recebe um comando serial ele dispara o correspondente no linux. A ferramente xdotool permite simular o mouse e teclado, e com isso é possivel enviar comandos como aumentar/baixa volume pelas teclas especiais de volume, play/pause etc. Também é possível utilizar diretamente outros comandos do linux, como por exemplo desligar a tela ou fazer shutdown da máquina.
No exemplo acima estão comandos diretos no linux, comandos que simulam teclado ( xdotool key ... ) e comandos que simulam mouse (xdotool mousemove_relative e click ) .


Na figura acima esta uma amostra do programa recebendo os códugos. O primeiro campo é o código recebido pela IR, depois o numero de repetições, uma descrição do código, e o comando executado no linux.
Para algumas teclas do controle é simulado o uso de teclas de multimedia (volume, play, stop por exemplo), barra de rolagem do mouse, ou é executado algum comando específico, como desligar o sistema.

Ajuste do navegador

Foi encontrado um problema inesperado durante esse projeto: os navegadores de internet de forma geral não aceitam as teclas de acesso rápido multimidia (play, pause, stop...). Para fazer isso funcionar é necessário instalar um plugin no navegador. Não tive sucesso ao utilizar isso no firefox - o plugin não funcionava. No chrome o plugin funcionou corretamente e agora consigo controlar sites de musica, youtube e netflix com o controle remoto.

Falta deixar claro que estamos simulando tecla de atalho multimidia, que é possivel encontra em alguns teclado reais. Existe o padrão XF86 que são as telcas multimidia. Existe diferentes funções que podem ser acessadas com essas teclas.

Recepção Arduino

A comunicação com o Arduino é feita pela USB. Se o sistema não estiver encontrado o arduino verifique o nome da porta que o arduino esta instalado e corrija no programa de recepção em python. Também verifique se o acesso a USB esta liberado. Falta de acesso é um problema comum com o Arduino, existe bastane informação sobre isso na internet.


Nenhum comentário:

Postar um comentário