DIY clicker

Recientemente, un amigo me pidi贸 que ayudara con una tarea: controlar una computadora con un reproductor de audio instalado en una computadora port谩til con Windows usando un peque帽o control remoto de hardware. Ped铆 todo tipo de controles remotos IR que no ofrecieran. Y para hacer el AVR-e, del que le queda un n煤mero considerable, es necesario conectarlo lentamente.


Declaraci贸n del problema.


La tarea, obviamente, se divide en dos partes:


  • Hardware de microcontrolador, y
  • Software que se ejecuta en una computadora y controla lo que contiene.

Dado que estamos trabajando con AVR, 驴por qu茅 no Arduino?


Nosotros planteamos el problema.
Plataforma de hardware:
HW1. La gesti贸n se realiza mediante botones sin fijaci贸n;
HW2. Servimos 3 botones (en general, a cu谩ntos no les importa);
HW3. Al presionar se considera que se mantiene presionado el bot贸n durante al menos 100 milisegundos;
HW4. Las prensas m谩s largas se ignoran. No se procesa m谩s de 1 bot贸n a la vez;
HW5. Cuando se presiona un bot贸n, se inicia una determinada acci贸n en la computadora;
HW6. Proporcione una interfaz de comunicaci贸n con una computadora a trav茅s del convertidor serial / USB incorporado;
Plataforma de software:
SW1. Proporcionar una interfaz de comunicaci贸n con una computadora a trav茅s de un puerto serie seleccionable;
SW2. Convierta los comandos que llegan a trav茅s de la interfaz de comunicaci贸n a los eventos del sistema operativo entregados al reproductor de audio deseado.
SW3. Pausa el procesamiento del comando. Incluyendo un comando desde el control remoto.


Bueno, hay un requisito adicional: si esto no introduce una inversi贸n de tiempo seria, haga que las soluciones sean lo m谩s universales posible.


Dise帽o y soluci贸n


Hw1


Los botones del bot贸n permanecen en la posici贸n "presionada" por un corto tiempo. Adem谩s, los botones pueden sonar (es decir, generar muchos desencadenantes en un corto per铆odo de tiempo debido a un contacto inestable).
No tiene sentido conectarlos a las interrupciones: se necesitan tiempos de respuesta incorrectos para molestarse con esto. Leeremos su estado de pines digitales. Para garantizar una lectura estable del bot贸n en el estado no comprimido, es necesario conectar el pin de entrada a tierra (pull-down) o al poder (pull-up) a trav茅s de una resistencia pull-up. Usando la resistencia pull-up incorporada, no crearemos un elemento discreto adicional en el circuito. Por un lado, conectamos el bot贸n a nuestra entrada, el otro, al suelo. Aqu铆 est谩 el resultado:
Diagrama de conexi贸n de botones
Y as铆, para cada bot贸n.


Hw2


Hay varios botones, por lo que necesitamos una cierta cantidad de registros uniformes sobre c贸mo sondear los botones y qu茅 hacer si se presiona. Miramos hacia la encapsulaci贸n y hacemos la clase Button del Button , que contiene el n煤mero del pin desde el que se realiza la encuesta (y la inicializa), y el comando que debe enviarse al puerto. Nos ocuparemos de c贸mo es el equipo m谩s adelante.


La clase de bot贸n se ver谩 as铆:


C贸digo de clase de bot贸n
 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; }; 

Despu茅s de este paso, nuestros botones se han vuelto universales y sin rostro, pero puede trabajar con ellos de la misma manera.


Junte los botones y as铆gneles los pines:


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

La inicializaci贸n de todos los botones se realiza llamando al m茅todo Begin() para cada bot贸n:


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

Para determinar qu茅 bot贸n se presiona, iteraremos sobre los botones y verificaremos si se presiona algo. Retornamos el 铆ndice del bot贸n, o uno de los valores especiales: "no se presiona nada" y "se presiona m谩s de un bot贸n". Los valores especiales, por supuesto, no pueden superponerse con n煤meros de bot贸n 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


Los botones se sondear谩n con un cierto per铆odo (por ejemplo, 10 ms), y asumiremos que la presi贸n se produjo si se mantuvo el mismo bot贸n (y exactamente uno) durante un n煤mero determinado de ciclos de sondeo. Divida el tiempo de fijaci贸n (100 ms) por el per铆odo de sondeo (10 ms), obtenemos 10.
Comenzaremos un contador de decremento, en el que escribiremos 10 en la primera fijaci贸n de prensado, y decremento en cada per铆odo. Tan pronto como pasa de 1 a 0, comenzamos a procesar (ver HW5)


Hw4


Si el contador ya es 0, no se toman medidas.


Hw5


Como se mencion贸 anteriormente, un comando ejecutable est谩 asociado con cada bot贸n. Debe transmitirse a trav茅s de la interfaz de comunicaci贸n.


En esta etapa, puede implementar una estrategia de teclado.


Implementaci贸n del bucle 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


La interfaz de comunicaci贸n debe ser clara tanto para el remitente como para el destinatario. Dado que la interfaz en serie tiene una unidad de transferencia de datos de 1 byte y tiene sincronizaci贸n de bytes, tiene poco sentido cercar algo complicado y limitarnos a transmitir un byte por comando. Para facilitar la depuraci贸n, transferiremos un car谩cter ASCII por comando.


Implementaci贸n Arduino


Ahora recogemos. El c贸digo de implementaci贸n completo se muestra a continuaci贸n debajo del spoiler. Para expandirlo, es suficiente especificar el c贸digo ASCII del nuevo comando y adjuntarle un bot贸n.
Por supuesto, ser铆a posible indicar expl铆citamente un c贸digo de s铆mbolo para cada bot贸n, pero no haremos esto: el nombramiento de comandos nos ser谩 煤til al implementar un cliente para una PC.


Implementaci贸n 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); } 

Y s铆, hice otro bot贸n para poder pausar la transferencia de comandos al cliente.


Cliente para PC


Pasamos a la segunda parte.
Dado que no necesitamos una interfaz compleja y vinculante para Windows, podemos ir de diferentes maneras, como desee: WinAPI, MFC, Delphi, .NET (Windows Forms, WPF, etc.) o consolas en las mismas plataformas ( bueno, excepto para MFC).


SW1


Este requisito se cierra mediante la comunicaci贸n con el puerto serie en la plataforma de software seleccionada: conectarse al puerto, leer bytes, procesar bytes.


SW2


Quiz谩s todos vieron teclados con teclas multimedia. Cada tecla del teclado, incluida la multimedia, tiene su propio c贸digo. La soluci贸n m谩s simple a nuestro problema es simular las pulsaciones de teclas multimedia en el teclado. Los c贸digos clave se pueden encontrar en la fuente original: MSDN . Queda por aprender c贸mo enviarlos al sistema. Esto tampoco es dif铆cil: hay una funci贸n SendInput en WinAPI.
Cada pulsaci贸n de tecla es dos eventos: presionar y soltar.
Si usamos C / C ++, simplemente podemos incluir los archivos de encabezado. En otros idiomas, el reenv铆o de llamadas debe hacerse. Entonces, por ejemplo, cuando desarrolle en .NET deber谩 importar la funci贸n especificada y describir los argumentos. Eleg铆 .NET por la conveniencia de desarrollar una interfaz.
Seleccion茅 del proyecto solo la parte sustantiva, que se reduce a una clase: Internals .
Aqu铆 est谩 su c贸digo:


C贸digo interno de clase
  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); } } 

Primero, describe las estructuras de datos (solo lo que est谩 relacionado con la entrada del teclado se corta, ya que lo simulamos), y la importaci贸n SendInput .
El campo de inputs es una matriz de dos elementos que se utilizar谩n para generar eventos de teclado. No tiene sentido asignarlo din谩micamente si la arquitectura de la aplicaci贸n supone que SendKey no se SendKey en varios subprocesos.
En realidad, el asunto t茅cnico es a煤n m谩s: completamos los campos de estructura correspondientes con el c贸digo de clave virtual y lo enviamos a la cola de entrada del sistema operativo.


SW3


El requisito se cierra de manera muy simple. Se levanta la bandera y se procesa otro comando de una manera especial: la bandera cambia al estado l贸gico opuesto. Si est谩 configurado, el resto de los comandos se ignoran.


En lugar de una conclusi贸n


La mejora se puede hacer sin fin, pero esa es otra historia. No presento aqu铆 un proyecto de cliente de Windows, porque proporciona un amplio vuelo de imaginaci贸n.
Para controlar el reproductor multimedia, le enviamos un conjunto de "pulsaciones de teclas", si necesita administrar presentaciones, otro. Puede crear m贸dulos de control, ensamblarlos est谩ticamente o como complementos. En general, muchas cosas son posibles. Lo principal es el deseo.


Gracias por su atencion

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


All Articles