DIY Clicker

Kürzlich bat mich ein Freund, bei einer Aufgabe zu helfen: einen Computer mit einem auf einem Windows-Laptop installierten Audio-Player mit einer kleinen Hardware-Fernbedienung zu steuern. Ich bat alle Arten von IR-Fernbedienungen, nicht anzubieten. Und um den AVR-e herzustellen, von dem er noch eine beträchtliche Anzahl hat, muss er langsam angebracht werden.


Erklärung des Problems


Die Aufgabe ist offensichtlich in zwei Teile unterteilt:


  • Mikrocontroller-Hardware und
  • Software, die auf einem Computer ausgeführt wird und steuert, was sich darauf befindet.

Warum nicht Arduino, da wir mit AVR arbeiten?


Wir stellen das Problem.
Hardwareplattform:
HW1. Die Verwaltung erfolgt über Tasten ohne Fixierung;
HW2. Wir bedienen 3 Knöpfe (im Allgemeinen, wie viele haben nichts dagegen);
HW3. Durch Drücken wird die Taste mindestens 100 Millisekunden lang gedrückt.
HW4. Längere Druckvorgänge werden ignoriert. Es wird nicht mehr als eine Taste gleichzeitig verarbeitet.
HW5. Wenn eine Taste gedrückt wird, wird eine bestimmte Aktion auf dem Computer gestartet.
HW6. Bereitstellung einer Kommunikationsschnittstelle mit einem Computer über den integrierten Serial / USB-Konverter;
Softwareplattform:
SW1. Bereitstellung einer Kommunikationsschnittstelle mit einem Computer über eine auswählbare serielle Schnittstelle;
SW2. Konvertieren Sie die über die Kommunikationsschnittstelle eingehenden Befehle in die Ereignisse des Betriebssystems, die an den gewünschten Audioplayer gesendet werden.
SW3. Unterbrechen Sie die Befehlsverarbeitung. Einschließlich eines Befehls von der Fernbedienung.


Nun, es gibt eine zusätzliche Anforderung: Wenn dies keinen ernsthaften Zeitaufwand bedeutet, machen Sie die Lösungen so universell wie möglich.


Design und Lösung


Hw1


Die Tasten der Taste bleiben für kurze Zeit in der Position "gedrückt". Darüber hinaus können die Tasten klappern (dh aufgrund instabilen Kontakts in kurzer Zeit viele Auslöser erzeugen).
Es macht keinen Sinn, sie mit Interrupts zu verbinden - dafür sind die falschen Antwortzeiten erforderlich. Wir werden ihren Status von digitalen Pins lesen. Um ein stabiles Ablesen der Taste im ungedrückten Zustand zu gewährleisten, muss der Eingangsstift über einen Pull-up-Widerstand mit Masse (Pulldown) oder Strom (Pull-Up) verbunden werden. Mit dem eingebauten Pull-up-Widerstand wird kein zusätzliches diskretes Element in der Schaltung hergestellt. Einerseits verbinden wir den Knopf mit unserem Eingang, andererseits - mit dem Boden. Hier ist das Ergebnis:
Schaltplan
Und so - für jeden Knopf.


Hw2


Es gibt mehrere Schaltflächen, daher benötigen wir eine bestimmte Anzahl einheitlicher Aufzeichnungen darüber, wie Schaltflächen abgefragt werden und was zu tun ist, wenn sie gedrückt werden. Wir schauen auf die Kapselung und erstellen die Button Klasse des Button , die die Nummer des Pins enthält, von dem aus die Umfrage durchgeführt wird (und der sie selbst initialisiert), und den Befehl, der an den Port gesendet werden muss. Wir werden uns später mit dem Team befassen.


Die Button-Klasse sieht ungefähr so ​​aus:


Tastenklassencode
 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; }; 

Nach diesem Schritt sind unsere Tasten universell und gesichtslos geworden, aber Sie können auf die gleiche Weise damit arbeiten.


Setzen Sie die Tasten zusammen und weisen Sie ihnen Stifte zu:


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

Die Initialisierung aller Schaltflächen erfolgt durch Aufrufen der Begin() -Methode für jede Schaltfläche:


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

Um festzustellen, welche Taste gedrückt wird, werden wir die Tasten durchlaufen und prüfen, ob etwas gedrückt wird. Wir geben den Tastenindex oder einen der speziellen Werte zurück: "nichts wird gedrückt" und "mehr als eine Taste wird gedrückt". Spezielle Werte dürfen sich natürlich nicht mit gültigen Tastennummern überschneiden.


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


Die Tasten werden mit einem bestimmten Zeitraum (z. B. 10 ms) abgefragt, und wir gehen davon aus, dass das Drücken erfolgt ist, wenn dieselbe Taste (und genau eine) für eine bestimmte Anzahl von Abfragezyklen gedrückt wurde. Teilen Sie die Fixierungszeit (100 ms) durch die Abfragezeit (10 ms), wir erhalten 10.
Wir starten einen Dekrementierungszähler, in den wir bei der ersten Fixierung des Pressens 10 schreiben und in jeder Periode dekrementieren. Sobald es von 1 auf 0 geht, beginnen wir mit der Verarbeitung (siehe HW5)


Hw4


Wenn der Zähler bereits 0 ist, wird keine Aktion ausgeführt.


Hw5


Wie oben erwähnt, ist jeder Schaltfläche ein ausführbarer Befehl zugeordnet. Es muss über die Kommunikationsschnittstelle übertragen werden.


In dieser Phase können Sie eine Tastaturstrategie implementieren.


Implementierung der Hauptschleife
 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


Die Kommunikationsschnittstelle sollte sowohl für den Absender als auch für den Empfänger klar sein. Da die serielle Schnittstelle eine Datenübertragungseinheit von 1 Byte und eine Bytesynchronisation hat, ist es wenig sinnvoll, etwas Kompliziertes einzuschränken und uns darauf zu beschränken, ein Byte pro Befehl zu übertragen. Zur Vereinfachung des Debuggens übertragen wir ein ASCII-Zeichen pro Befehl.


Arduino-Implementierung


Jetzt sammeln wir. Der vollständige Implementierungscode wird unten unter dem Spoiler angezeigt. Um es zu erweitern, reicht es aus, den ASCII-Code des neuen Befehls anzugeben und eine Schaltfläche daran anzuhängen.
Natürlich wäre es möglich, nur explizit einen Symbolcode für jede Schaltfläche anzugeben, aber wir werden dies nicht tun: Die Befehlsbenennung ist für uns nützlich, wenn wir einen Client für einen PC implementieren.


Vollständige Implementierung
 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); } 

Und ja, ich habe eine weitere Schaltfläche erstellt, um die Übertragung von Befehlen an den Client anzuhalten.


Client für PC


Wir gehen zum zweiten Teil über.
Da wir keine komplexe Benutzeroberfläche und keine Bindung an Windows benötigen, können wir auf verschiedene Arten vorgehen: WinAPI, MFC, Delphi, .NET (Windows Forms, WPF usw.) oder Konsolen auf denselben Plattformen ( gut, außer MFC).


SW1


Diese Anforderung wird durch die Kommunikation mit der seriellen Schnittstelle auf der ausgewählten Softwareplattform geschlossen: Verbindung zur Schnittstelle herstellen, Bytes lesen, Bytes verarbeiten.


SW2


Vielleicht hat jeder Tastaturen mit Multimedia-Tasten gesehen. Jede Taste auf der Tastatur, einschließlich der Multimedia-Taste, verfügt über einen eigenen Code. Die einfachste Lösung für unser Problem besteht darin, die Tastenanschläge von Multimedia-Tasten auf der Tastatur zu simulieren. Schlüsselcodes finden Sie in der Originalquelle - MSDN . Es bleibt zu lernen, wie man sie an das System sendet. Dies ist auch nicht schwierig: In WinAPI gibt es eine SendInput- Funktion.
Jeder Tastendruck besteht aus zwei Ereignissen: Drücken und Loslassen.
Wenn wir C / C ++ verwenden, können wir einfach die Header-Dateien einschließen. In anderen Sprachen muss die Anrufweiterleitung erfolgen. Wenn Sie beispielsweise in .NET entwickeln, müssen Sie die angegebene Funktion importieren und die Argumente beschreiben. Ich habe mich für .NET entschieden, um eine Schnittstelle zu entwickeln.
Ich habe aus dem Projekt nur den inhaltlichen Teil ausgewählt, der sich auf eine Klasse beschränkt: Internals .
Hier ist sein Code:


Interna mit Klassencode
  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); } } 

Zunächst werden die Datenstrukturen (nur was mit der Tastatureingabe zusammenhängt, wird abgeschnitten, da wir sie simulieren) und der SendInput Import SendInput .
Das inputs besteht aus zwei Elementen, mit denen Tastaturereignisse generiert werden. Es macht keinen Sinn, es dynamisch zuzuweisen, wenn die Anwendungsarchitektur davon ausgeht, dass SendKey nicht in mehreren Threads SendKey wird.
Eigentlich ist die technische Angelegenheit weiter: Wir füllen die entsprechenden Strukturfelder mit dem virtuellen Schlüsselcode aus und senden ihn an die Eingabewarteschlange des Betriebssystems.


SW3


Die Anforderung schließt sehr einfach. Das Flag wird gehisst und ein anderer Befehl wird auf besondere Weise verarbeitet: Das Flag wechselt in den entgegengesetzten logischen Zustand. Wenn es gesetzt ist, werden die restlichen Befehle ignoriert.


Anstelle einer Schlussfolgerung


Verbesserungen können endlos durchgeführt werden, aber das ist eine andere Geschichte. Ich präsentiere hier kein Windows-Client-Projekt, da es einen großen Vorstellungsspielraum bietet.
Um den Media Player zu steuern, senden wir einen Satz Tastenanschläge, wenn Sie Präsentationen verwalten müssen, einen anderen. Sie können Steuermodule erstellen, entweder statisch oder als Plug-Ins zusammenbauen. Im Allgemeinen sind viele Dinge möglich. Die Hauptsache ist das Verlangen.


Vielen Dank für Ihre Aufmerksamkeit.

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


All Articles