Clicker DIY

Recentemente, um amigo me pediu para ajudar em uma tarefa: controlar um computador com um reprodutor de áudio instalado em um laptop Windows usando um pequeno controle remoto de hardware. Eu pedi para todos os tipos de controles remotos de RI não oferecerem. E para fazer o AVR-e, do qual ele ainda tem um número considerável, é necessário anexar lentamente.


Declaração do problema


A tarefa, obviamente, é dividida em duas partes:


  • Hardware de microcontrolador e
  • Software que roda em um computador e controla o que está nele.

Como estamos trabalhando com o AVR, por que não o Arduino?


Nós colocamos o problema.
Plataforma de hardware:
HW1. O gerenciamento é realizado por botões sem correção;
HW2. Servimos 3 botões (em geral, quantos não se importam);
HW3. Pressionar é considerado pressionado o botão por pelo menos 100 milissegundos;
HW4. Prensas mais longas são ignoradas. O processamento de mais de 1 botão por vez não é realizado;
HW5. Quando um botão é pressionado, uma determinada ação é iniciada no computador;
HW6. Forneça uma interface de comunicação com um computador através do conversor serial / USB embutido;
Plataforma de software:
SW1. Forneça uma interface de comunicação com o computador através de uma porta serial selecionável;
SW2. Converta os comandos que passam pela interface de comunicação nos eventos do sistema operacional entregues no reprodutor de áudio desejado.
SW3. Pausar o processamento do comando. Incluindo um comando do controle remoto.


Bem, há um requisito adicional: se isso não introduzir um investimento sério de tempo, torne as soluções o mais universal possível.


Projeto e Solução


Hw1


Os botões do botão permanecem na posição "pressionada" por um curto período de tempo. Além disso, os botões podem chacoalhar (ou seja, gerar muitos disparos em um curto período de tempo devido ao contato instável).
Não faz sentido conectá-los a interrupções - são necessários tempos de resposta incorretos para se preocupar com isso. Leremos o status dos pinos digitais. Para garantir uma leitura estável do botão no estado não comprimido, é necessário conectar o pino de entrada ao terra (pull-down) ou à energia (pull-up) através de um resistor pull-up. Usando o resistor pull-up embutido, não criaremos um elemento discreto adicional no circuito. Por um lado, conectamos o botão à nossa entrada e a outra - ao solo. Aqui está o resultado:
Diagrama de conexão de botão
E assim - para cada botão.


Hw2


Como existem vários botões, precisamos de uma certa quantidade de registros uniformes sobre como pesquisar os botões e o que fazer se ele for pressionado. Nós olhamos para o encapsulamento e criamos a classe Button do Button , que contém o número do pino a partir do qual a pesquisa está sendo conduzida (e ela própria a inicializa) e o comando que deve ser enviado à porta. Trataremos de como é a equipe mais tarde.


A classe de botão será mais ou menos assim:


Código de classe do botão
 class Button { public: Button(uint8_t pin, ::Command command) : pin(pin), command(command) {} void Begin() { pinMode(pin, INPUT); digitalWrite(pin, 1); } bool IsPressed() { return !digitalRead(pin); } ::Command Command() const { return command; } private: uint8_t pin; ::Command command; }; 

Após esta etapa, nossos botões se tornaram universais e sem rosto, mas você pode trabalhar com eles da mesma maneira.


Coloque os botões juntos e atribua a eles pinos:


 Button buttons[] = { Button(A0, Command::Previous), Button(A1, Command::PauseResume), Button(A2, Command::Next), }; 

A inicialização de todos os botões é feita chamando o método Begin() para cada botão:


 for (auto &button : buttons) { button.Begin(); } 

Para determinar qual botão foi pressionado, iremos percorrer os botões e verificar se algo foi pressionado. Retornamos o índice do botão ou um dos valores especiais: "nada é pressionado" e "mais de um botão é pressionado". É claro que valores especiais não podem se sobrepor a números de botão válidos.


GetPressed ()
 int GetPressed() { int index = PressedNothing; for (byte i = 0; i < ButtonsCount; ++i) { if (buttons[i].IsPressed()) { if (index == PressedNothing) { index = i; } else { return PressedMultiple; } } } return index; } 

Hw3


Os botões serão pesquisados ​​com um determinado período (digamos, 10 ms), e assumiremos que a pressão ocorreu se o mesmo botão (e exatamente um) foi pressionado por um determinado número de ciclos de pesquisa. Divida o tempo de fixação (100 ms) pelo período de pesquisa (10 ms), obtemos 10.
Iniciaremos um contador de decréscimo, no qual escrevemos 10 na primeira fixação de prensagem e decrescemos a cada período. Assim que passa de 1 para 0, iniciamos o processamento (consulte HW5)


Hw4


Se o contador já for 0, nenhuma ação será tomada.


Hw5


Como mencionado acima, um comando executável é associado a cada botão. Ele deve ser transmitido através da interface de comunicação.


Nesta fase, você pode implementar uma estratégia de teclado.


Implementação do loop principal
 void HandleButtons() { static int CurrentButton = PressedNothing; static byte counter; int button = GetPressed(); if (button == PressedMultiple || button == PressedNothing) { CurrentButton = button; counter = -1; return; } if (button == CurrentButton) { if (counter > 0) { if (--counter == 0) { InvokeCommand(buttons[button]); return; } } } else { CurrentButton = button; counter = PressInterval / TickPeriod; } } void loop() { HandleButtons(); delay(TickPeriod); } 

Hw6


A interface de comunicação deve ser clara para o remetente e o destinatário. Como a interface serial possui uma unidade de transferência de dados de 1 byte e sincronização de bytes, faz pouco sentido cercar algo complicado e nos limitar a transmitir um byte por comando. Por conveniência de depuração, transferiremos um caractere ASCII por comando.


Implementação do Arduino


Agora nós coletamos. O código de implementação completo é mostrado abaixo no spoiler. Para expandi-lo, basta especificar o código ASCII do novo comando e anexar um botão a ele.
Obviamente, seria possível indicar explicitamente um código de símbolo para cada botão, mas não faremos isso: a nomeação de comandos será útil para a implementação de um cliente para um PC.


Implementação completa
 const int TickPeriod = 10; //ms const int PressInterval = 100; //ms enum class Command : char { None = 0, Previous = 'P', Next = 'N', PauseResume = 'C', SuspendResumeCommands = '/', }; class Button { public: Button(uint8_t pin, Command command) : pin(pin), command(command) {} void Begin() { pinMode(pin, INPUT); digitalWrite(pin, 1); } bool IsPressed() { return !digitalRead(pin); } Command GetCommand() const { return command; } private: uint8_t pin; Command command; }; Button buttons[] = { Button(A0, Command::Previous), Button(A1, Command::PauseResume), Button(A2, Command::Next), Button(12, Command::SuspendResumeCommands), }; const byte ButtonsCount = sizeof(buttons) / sizeof(buttons[0]); void setup() { for (auto &button : buttons) { button.Begin(); } Serial.begin(9600); } enum { PressedNothing = -1, PressedMultiple = -2, }; int GetPressed() { int index = PressedNothing; for (byte i = 0; i < ButtonsCount; ++i) { if (buttons[i].IsPressed()) { if (index == PressedNothing) { index = i; } else { return PressedMultiple; } } } return index; } void InvokeCommand(const class Button& button) { Serial.write((char)button.GetCommand()); } void HandleButtons() { static int CurrentButton = PressedNothing; static byte counter; int button = GetPressed(); if (button == PressedMultiple || button == PressedNothing) { CurrentButton = button; counter = -1; return; } if (button == CurrentButton) { if (counter > 0) { if (--counter == 0) { InvokeCommand(buttons[button]); return; } } } else { CurrentButton = button; counter = PressInterval / TickPeriod; } } void loop() { HandleButtons(); delay(TickPeriod); } 

E sim, eu criei outro botão para poder pausar a transferência de comandos para o cliente.


Cliente para PC


Passamos para a segunda parte.
Como não precisamos de uma interface complexa e vinculativa ao Windows, podemos seguir de diferentes maneiras, como você deseja: WinAPI, MFC, Delphi, .NET (Windows Forms, WPF etc.) ou consoles nas mesmas plataformas ( bem, exceto para o MFC).


SW1


Este requisito é fechado através da comunicação com a porta serial na plataforma de software selecionada: conecte-se à porta, leia bytes, bytes de processo.


SW2


Talvez todo mundo tenha visto teclados com teclas multimídia. Cada tecla do teclado, incluindo a multimídia, possui seu próprio código. A solução mais simples para o nosso problema é simular o pressionamento de teclas de teclas multimídia no teclado. Os códigos de chave podem ser encontrados na fonte original - MSDN . Resta aprender como enviá-los para o sistema. Isso também não é difícil: há uma função SendInput no WinAPI.
Cada pressionamento de tecla é dois eventos: pressionar e liberar.
Se usarmos C / C ++, podemos simplesmente incluir os arquivos de cabeçalho. Em outros idiomas, o encaminhamento de chamadas deve ser feito. Assim, por exemplo, ao desenvolver no .NET, você precisará importar a função especificada e descrever os argumentos. Eu escolhi o .NET pela conveniência de desenvolver uma interface.
Selecionei no projeto apenas a parte substantiva, que se resume a uma classe: Internals .
Aqui está o código dele:


Código interno da classe
  internal class Internals { [StructLayout(LayoutKind.Sequential)] [DebuggerDisplay("{Type} {Data}")] private struct INPUT { public uint Type; public KEYBDINPUT Data; public const uint Keyboard = 1; public static readonly int Size = Marshal.SizeOf(typeof(INPUT)); } [StructLayout(LayoutKind.Sequential)] [DebuggerDisplay("Vk={Vk} Scan={Scan} Flags={Flags} Time={Time} ExtraInfo={ExtraInfo}")] private struct KEYBDINPUT { public ushort Vk; public ushort Scan; public uint Flags; public uint Time; public IntPtr ExtraInfo; private long spare; } [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint numberOfInputs, INPUT[] inputs, int sizeOfInputStructure); private static INPUT[] inputs = { new INPUT { Type = INPUT.Keyboard, Data = { Flags = 0 // Push } }, new INPUT { Type = INPUT.Keyboard, Data = { Flags = 2 // Release } } }; public static void SendKey(Keys key) { inputs[0].Data.Vk = (ushort) key; inputs[1].Data.Vk = (ushort) key; SendInput(2, inputs, INPUT.Size); } } 

Primeiro, ele descreve as estruturas de dados (apenas o que está relacionado à entrada do teclado é cortado, já que nós a simulamos) e a SendInput importada.
O campo de inputs é uma matriz de dois elementos que serão usados ​​para gerar eventos de teclado. Não faz sentido alocá-lo dinamicamente se a arquitetura do aplicativo assumir que o SendKey não será SendKey em vários threads.
Na verdade, o assunto técnico é mais aprofundado: preenchemos os campos correspondentes da estrutura com o código da chave virtual e o enviamos para a fila de entrada do sistema operacional.


SW3


O requisito fecha muito simplesmente. O sinalizador é gerado e outro comando é processado de uma maneira especial: o sinalizador muda para o estado lógico oposto. Se estiver definido, os comandos restantes serão ignorados.


Em vez de uma conclusão


Melhorar pode ser feito sem parar, mas isso é outra história. Não apresento aqui um projeto de cliente do Windows, porque ele oferece uma grande fuga de imaginação.
Para controlar o media player, enviamos um conjunto de "pressionamentos de teclas" de teclas, se você precisar gerenciar apresentações, outro. Você pode criar módulos de controle, montá-los estaticamente ou como plug-ins. Em geral, muitas coisas são possíveis. O principal é o desejo.


Obrigado pela atenção.

Source: https://habr.com/ru/post/pt422203/


All Articles