Clicker DIY

Récemment, un ami m'a demandé d'aider avec une tâche: contrôler un ordinateur avec un lecteur audio installé sur un ordinateur portable Windows à l'aide d'une petite télécommande matérielle. J'ai demandé à toutes sortes de télécommandes IR de ne pas offrir. Et pour fabriquer l'AVR-e, dont il lui reste un nombre considérable, il faut le fixer lentement.


Énoncé du problème


La tâche est évidemment divisée en deux parties:


  • Matériel du microcontrôleur, et
  • Logiciel qui s'exécute sur un ordinateur et contrôle ce qui s'y trouve.

Puisque nous travaillons avec AVR, alors pourquoi pas Arduino?


Nous posons le problème.
Plateforme matérielle:
HW1. La gestion s'effectue par boutons sans fixation;
HW2. Nous servons 3 boutons (en général, combien ne me dérange pas);
HW3. On considère que le fait de maintenir le bouton enfoncé pendant au moins 100 millisecondes;
HW4. Les pressions plus longues sont ignorées. Le traitement de plus d'un bouton à la fois n'est pas effectué;
HW5. Lorsqu'un bouton est enfoncé, une certaine action est lancée sur l'ordinateur;
HW6. Fournir une interface de communication avec un ordinateur via le convertisseur série / USB intégré;
Plateforme logicielle:
SW1. Fournir une interface de communication avec un ordinateur via un port série sélectionnable;
SW2. Convertissez les commandes provenant de l'interface de communication en événements du système d'exploitation transmis au lecteur audio souhaité.
SW3. Suspendre le traitement des commandes. Y compris une commande de la télécommande.


Eh bien, il y a une exigence supplémentaire: si cela n'introduit pas un investissement sérieux en temps, rendez les solutions aussi universelles que possible.


Conception et solution


Hw1


Les boutons du bouton restent en position "enfoncée" pendant un court instant. De plus, les boutons peuvent vibrer (c'est-à-dire générer de nombreux déclencheurs en peu de temps en raison d'un contact instable).
Cela n'a aucun sens de les connecter à des interruptions - les mauvais temps de réponse sont nécessaires pour s'en occuper. Nous lirons leur statut à partir de broches numériques. Pour assurer une lecture stable du bouton à l'état non pressé, il est nécessaire de connecter la broche d'entrée à la masse (pull-down) ou à l'alimentation (pull-up) via une résistance pull-up. En utilisant la résistance de rappel intégrée, nous ne ferons pas d'élément discret supplémentaire dans le circuit. D'une part, nous connectons le bouton à notre entrée, l'autre - au sol. Voici le résultat:
Schéma de connexion des boutons
Et donc - pour chaque bouton.


Hw2


Il y a plusieurs boutons, nous avons donc besoin d'une certaine quantité d'enregistrements uniformes sur la façon d'interroger les boutons et ce qu'il faut faire si vous appuyez dessus. Nous regardons vers l'encapsulation et faisons la classe Button du Button , qui contient le numéro de la broche à partir de laquelle l'enquête est menée (et l'initialise elle-même), et la commande qui doit être envoyée au port. Nous traiterons de la composition de l'équipe plus tard.


La classe de boutons ressemblera à ceci:


Code de classe de bouton
 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; }; 

Après cette étape, nos boutons sont devenus universels et sans visage, mais vous pouvez travailler avec eux de la même manière.


Assemblez les boutons et assignez-leur des broches


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

L'initialisation de tous les boutons se fait en appelant la méthode Begin() pour chaque bouton:


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

Afin de déterminer quel bouton est pressé, nous allons parcourir les boutons et vérifier si quelque chose est pressé. Nous retournons l'index du bouton, ou l'une des valeurs spéciales: "rien n'est pressé" et "plus d'un bouton est pressé". Bien entendu, les valeurs spéciales ne peuvent pas se chevaucher avec des numéros de bouton valides.


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


Les boutons seront interrogés avec une certaine période (disons, 10 ms), et nous supposerons que la presse a eu lieu si le même bouton (et exactement un) a été maintenu pendant un nombre donné de cycles d'interrogation. Divisez le temps de fixation (100 ms) par la période d'interrogation (10 ms), nous obtenons 10.
Nous allons commencer un compteur de décrémentation, dans lequel nous écrivons 10 à la première fixation de pressage, et décrémentons à chaque période. Dès qu'il passe de 1 à 0, nous commençons le traitement (voir HW5)


Hw4


Si le compteur est déjà 0, aucune action n'est effectuée.


Hw5


Comme mentionné ci-dessus, une commande exécutable est associée à chaque bouton. Il doit être transmis via l'interface de communication.


À ce stade, vous pouvez implémenter une stratégie de clavier.


Implémentation de la boucle principale
 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


L'interface de communication doit être claire pour l'expéditeur et le destinataire. Étant donné que l'interface série a une unité de transfert de données de 1 octet et dispose d'une synchronisation d'octets, il est peu logique de clôturer quelque chose de compliqué et de nous limiter à transmettre un octet par commande. Pour faciliter le débogage, nous transférerons un caractère ASCII par commande.


Implémentation Arduino


Maintenant, nous collectons. Le code d'implémentation complet est indiqué ci-dessous sous le spoiler. Pour la développer, il suffit de spécifier le code ASCII de la nouvelle commande et d'y attacher un bouton.
Bien sûr, il serait possible simplement d'indiquer explicitement un code symbole pour chaque bouton, mais nous ne le ferons pas: la dénomination des commandes nous sera utile lors de l'implémentation d'un client pour un PC.


Mise en œuvre complète
 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); } 

Et oui, j'ai fait un autre bouton pour pouvoir suspendre le transfert des commandes au client.


Client pour PC


Nous passons à la deuxième partie.
Comme nous n'avons pas besoin d'une interface complexe et d'une liaison à Windows, nous pouvons aller de différentes manières, comme vous le souhaitez: WinAPI, MFC, Delphi, .NET (Windows Forms, WPF, etc.), ou des consoles sur les mêmes plates-formes ( bien, sauf pour MFC).


SW1


Cette exigence est fermée par la communication avec le port série sur la plate-forme logicielle sélectionnée: se connecter au port, lire les octets, traiter les octets.


SW2


Peut-être que tout le monde a vu des claviers avec des touches multimédias. Chaque touche du clavier, y compris la touche multimédia, a son propre code. La solution la plus simple à notre problème est de simuler les frappes de touches multimédias sur le clavier. Les codes clés se trouvent dans la source d'origine - MSDN . Reste à savoir comment les envoyer au système. Ce n'est pas non plus difficile: il existe une fonction SendInput dans WinAPI.
Chaque frappe correspond à deux événements: appuyer et relâcher.
Si nous utilisons C / C ++, nous pouvons simplement inclure les fichiers d'en-tête. Dans d'autres langues, le renvoi d'appel doit être effectué. Ainsi, par exemple, lors du développement sur .NET, vous devrez importer la fonction spécifiée et décrire les arguments. J'ai choisi .NET pour la commodité de développer une interface.
Je n'ai sélectionné dans le projet que la partie substantielle, qui se résume à une classe: les Internals .
Voici son code:


Internes de code de 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); } } 

Tout d'abord, il décrit les structures de données (seul ce qui est lié à l'entrée au clavier est coupé, car nous le SendInput ), et l'importation SendInput .
Le champ de inputs est un tableau de deux éléments qui sera utilisé pour générer des événements de clavier. Cela n'a aucun sens de l'allouer dynamiquement si l'architecture de l'application suppose que SendKey ne sera pas SendKey dans plusieurs threads.
En fait, la question technique est plus loin: nous remplissons les champs de structure correspondants avec le code de clé virtuelle et l'envoyons à la file d'attente d'entrée du système d'exploitation.


SW3


L'exigence se termine très simplement. Le drapeau est levé et une autre commande est traitée d'une manière spéciale: le drapeau passe à l'état logique opposé. S'il est défini, les autres commandes sont ignorées.


Au lieu d'une conclusion


L'amélioration peut se faire à l'infini, mais c'est une autre histoire. Je ne présente pas ici un projet client Windows, car il permet une grande envolée d'imagination.
Pour contrôler le lecteur multimédia, nous envoyons un ensemble de «frappes» de touches, si vous avez besoin de gérer des présentations, un autre. Vous pouvez créer des modules de contrôle, les assembler soit statiquement, soit sous forme de plug-ins. En général, beaucoup de choses sont possibles. L'essentiel est le désir.


Merci de votre attention.

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


All Articles