DIY答题器

最近,一个朋友要我完成一项任务:使用小型硬件遥控器控制在Windows笔记本电脑上安装了音频播放器的计算机。 我要求不要提供各种红外遥控器。 而要制造他还剩很多的AVR-e,必须缓慢安装。


问题陈述


显然,该任务分为两个部分:


  • 微控制器硬件,以及
  • 在计算机上运行并控制计算机内容的软件。

由于我们正在使用AVR,那么为什么不使用Arduino?


我们提出了问题。
硬件平台:
HW1。 通过按钮进行管理,无需固定;
硬件2。 我们提供3个按钮(通常不介意多少个按钮);
HW3。 按下可保持按钮至少100毫秒;
HW4。 长按会被忽略。 一次不能处理多个按钮;
HW5。 当按下按钮时,计算机上将启动特定的操作;
HW6。 通过内置的串行/ USB转换器提供与计算机的通信接口;
软件平台:
SW1。 通过可选的串行端口提供与计算机的通信接口;
SW2。 将通过通信接口发出的命令转换为传递给所需音频播放器的操作系统事件。
SW3。 暂停命令处理。 包括来自遥控器的命令。


好吧,还有一个额外的要求:如果这不花费大量时间,请使解决方案尽可能通用。


设计与解决方案


1号


按钮的按钮会在“按下”位置停留很短的时间。 另外,按钮可能会发出嘎嘎声(即由于不稳定的接触,在短时间内会产生大量触发)。
将它们连接到中断没有任何意义-需要花费错误的响应时间来解决这个问题。 我们将从数字引脚读取它们的状态。 为了确保在未按下状态下按钮的稳定读取,有必要通过上拉电阻将输入引脚接地(下拉)或电源(上拉)。 使用内置的上拉电阻,我们将不会在电路中制造额外的分立元件。 一方面,我们将按钮连接到输入,另一方面-接地。 结果如下:
按钮连接图
如此-每个按钮。


2号


有几个按钮,因此我们需要一定数量的统一记录,以了解如何轮询按钮以及按下按钮时的操作。 我们着眼于封装,并制作Button类,其中包含进行调查的引脚的编号(并且它本身将其初始化),以及必须发送到端口的命令。 稍后我们将处理团队的情况。


该按钮类将如下所示:


按钮类代码
 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; }; 

完成此步骤后,我们的按钮变得通用且不露面,但是您可以以相同的方式使用它们。


将按钮放在一起并为其分配引脚:


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

通过为每个按钮调用Begin()方法来完成所有按钮的初始化:


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

为了确定按下了哪个按钮,我们将遍历按钮并检查是否有按钮被按下。 我们返回按钮索引或以下特殊值之一:“什么都不按下”和“多个按钮被按下”。 当然,特殊值不能与有效按钮号重叠。


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

3号


将对按钮进行一定时间(例如10毫秒)的轮询,如果在给定的轮询周期内按住了相同的按钮(也就是一个按钮),我们将假定发生了按动。 将固定时间(100 ms)除以轮询周期(10 ms),得到10。
我们将启动一个减量计数器,在第一次压紧时写入10,并在每个周期减一。 从1到0,我们就开始处理(请参阅HW5)


4号


如果计数器已经为0,则不执行任何操作。


5号


如上所述,可执行命令与每个按钮相关联。 它必须通过通信接口进行传输。


在此阶段,您可以实施键盘策略。


主循环实现
 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); } 

6号


通信接口对于发送者和接收者都应该是清楚的。 由于串行接口的数据传输单元为1字节,并且具有字节同步功能,因此将一些复杂的东西围起来并限制我们只能每个命令传输一个字节几乎没有意义。 为了调试方便,我们将为每个命令传输一个ASCII字符。


Arduino实现


现在我们收集。 完整的实现代码如下所示,位于扰流板下方。 要对其进行扩展,只需指定新命令的ASCII码并在其上附加一个按钮即可。
当然,可以明确地为每个按钮指示符号代码,但是我们不会这样做:在为PC实施客户端时,命令命名对我们很有用。


全面实施
 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); } 

是的,我做了另一个按钮,可以暂停向客户端的命令传输。


PC客户端


我们转到第二部分。
由于我们不需要复杂的界面并绑定到Windows,因此可以按照您喜欢的方式采用不同的方式:WinAPI,MFC,Delphi,.NET(Windows窗体,WPF等),或相同平台上的控制台(好,除了MFC)。


SW1


通过与所选软件平台上的串行端口进行通信,可以关闭此要求:连接到该端口,读取字节,处理字节。


SW2


也许每个人都看到带有多媒体键的键盘。 键盘上的每个键(包括多媒体键)都有自己的代码。 解决我们问题的最简单方法是模拟键盘上多媒体键的击键。 关键代码可以在原始源MSDN中找到 。 还需要学习如何将它们发送到系统。 这也不难:WinAPI中有一个SendInput函数。
每个按键都是两个事件:按下和释放。
如果我们使用C / C ++,那么我们可以简单地包含头文件。 用其他语言,必须完成呼叫转移。 因此,例如,在.NET上进行开发时,您将必须导入指定的函数并描述参数。 我选择.NET是为了方便开发接口。
我从项目中仅选择了实质性部分,可以归结为一类: Internals
这是他的代码:


类代码内部
  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); } } 

首先,它描述了数据结构(因为我们模拟了它,因此仅切断了与键盘输入有关的内容),并且SendInput导入了SendInput
inputs字段是由两个元素组成的数组,将用于生成键盘事件。 如果应用程序体系结构假定不会在多个线程中SendKey则动态分配它是没有意义的。
实际上,技术问题还很深:我们用虚拟键代码填充相应的结构字段,然后将其发送到操作系统输入队列。


SW3


要求非常简单地关闭。 引发该标志并以特殊方式处理另一个命令:该标志切换到相反的逻辑状态。 如果已设置,则其余命令将被忽略。


而不是结论


改善可以无止境地进行,但这是另一回事。 我不在这里介绍Windows客户端项目,因为它提供了广泛的想象力。
为了控制媒体播放器,如果您需要管理演示文稿,则我们发送一组按键的“按键”,另一组。 您可以制作控制模块,以静态方式或作为插件组装它们。 通常,很多事情都是可能的。 最主要的是欲望。


谢谢您的关注。

Source: https://habr.com/ru/post/zh-CN422203/


All Articles