Кликер своими руками

Попросил меня на днях товарищ помочь с одной задачкой: управлять компом с аудиопроигрывателем, установленном на ноутбуке с Windows, с помощью маленького аппаратного пультика. Просил всякие ИК пульты не предлагать. И сделать AVR-е, коих у него осталось некоторое немалое количество, пристраивать потихоньку надо.

Постановка задачи

Задача, очевидно, делится на две части:

  • Железячное, работающее на микроконтроллере, и
  • Программное, работающее на компьютере и управляющее тем, что на нём находится.

Раз уж работаем с AVR, то почему бы не Arduino?

Поставим задачу.
Аппаратная платформа:
HW1. Управление ведётся кнопками без фиксации;
HW2. Обслуживаем 3 кнопки (в общем случае сколько не жалко);
HW3. Нажатием считается удерживание кнопки не менее 100 миллисекунд;
HW4. Более длинные нажатия игнорируются. Обработка более 1 кнопки за раз не выполняется;
HW5. При нажатии кнопки запускается некоторое действие на компьютере;
HW6. Обеспечить интерфейс связи с компьютером через встроенный Serial/USB-преобразователь;
Программная платформа:
SW1. Обеспечить интерфейс связи с компьютером через выбираемый последовательный порт;
SW2. Преобразовывать приходящие по интерфейсу связи команды в события операционной системы, доставляемые до нужного аудиоплеера.
SW3. Приостановка обработки команд. В том числе по команде с пульта.

Ну и дополнительное требование: если это не вносит серьёзных затрат времени, сделать решения максимально универсальными.

Проектирование и решение

HW1

Кнопки кнопки пребывают в положении «нажато» непродолжительное время. Кроме того, кнопки могут дребезжать (то есть формировать множество срабатываний за короткий промежуток времени из-за нестабильного контакта).
К прерываниям их подключать нет смысла — не те времена отклика нужны, чтобы этим заморачиваться. Будем читать их состояния с цифровых пинов. Для обеспечения стабильного чтения кнопки в ненажатом состоянии необходимо подключить входной пин к земле (pull-down) или к питанию (pull-up) через подтягивающий резистор. Воспользуемся встроенным pull-up резистором не будем делать дополнительный дискретный элемент на схеме. С одной стороны кнопку подключим к нашему входу, другой — к земле. Вот что получается:
Схема подключения кнопки
И так — для каждой кнопки.

HW2

Кнопок надо несколько, поэтому нам нужно некоторое количество однородных записей о том, как кнопки опрашивать и что надо делать, если она нажата. Смотрим в сторону инкапсуляции и делаем класс кнопки Button, который содержит в себе номер пина, с которого ведётся опрос (и сам же его инициализирует), и команду, которую надо послать в порт. С тем, что из себя представляет команда, разберёмся позже.

Класс кнопки будет выглядеть примерно так:

Код класса 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; }

HW3

Кнопки будем опрашивать с некоторым периодом (скажем, 10 мс), и будем считать, что нажатие произошло, если одна и та же кнопка (и ровно одна) удерживалась заданное количество циклов опроса. Делим время фиксации (100 мс) на период опроса (10 мс), получаем 10.
Заведём декрементный счётчик, в который записываем 10 при первой фиксации нажатия, и декрементируем на каждом периоде. Как только он переходит из 1 в 0, запускаем обработку (см HW5)

HW4

Если счётчик уже равен 0, никаких действий не выполняется.

HW5

Как было сказано выше, с каждой кнопкой ассоциирована выполняемая команда. Она должна быть передана через коммуникационный интерфейс.

На этом этапе можно реализовать стратегию работы клавиатурой.

Реализация главного цикла

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

Интерфейс связи должен быть понятным и отправителю, и получателю. Поскольку последовательный интерфейс имеет единицу передачи данных в 1 байт и имеет байтовую синхронизацию, то нет особого смысла городить что-то сложное и ограничимся передачей одного байта на команду. Для удобства отладки сделаем передачу одного ASCII-символа на команду.

Реализация на Arduino

Теперь собираем. Полный код реализации показан ниже под спойлером. Для его расширения достаточно указать ASCII-код новой команды и привязать к ней кнопку.
Можно, конечно, было бы просто для каждой кнопки явно указать код символа, но мы так делать не будем: именование команд нам пригодится при реализации клиента для ПК.

Полная реализация

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

И да, я сделал ещё одну кнопку, чтобы иметь возможность приостановки передачи команд на клиента.

Клиент для ПК

Переходим ко второй части.
Поскольку сложного интерфейса нам не надо, и привязка к Windows, то можно пойти разными путями, кому как нравится: WinAPI, MFC, Delphi, .NET (Windows Forms, WPF и т.д.), или же консольки на тех же платформах (ну, кроме MFC).

SW1

Данное требование закрывается через коммуникацию с последовательным портом на выбранной программной платформе: подключаемся к порту, читаем байты, обрабатываем байты.

SW2

Пожалуй, все видели клавиатуры с мультимедийными клавишами. Каждая клавиша на клавиатуре, в том числе мультимедийная, имеет свой код. Самое простое решение нашей задачи — это имитация нажатий мультимедийных клавиш на клавиатуре. С кодами клавиш можно ознакомиться в первоисточнике — MSDN. Осталось научиться их посылать в систему. Это тоже не сложно: есть в WinAPI функция SendInput.
Каждое нажатие клавиши — это два события: нажатие и отпускание.
Если мы пользуемся C/C++, то можно просто подключить заголовочные файлы. На других языках надо сделать проброс вызовов. Так, например, при разработке на .NET придётся импортировать указанную функцию и описать аргументы. Я выбрал .NET за удобство разработки интерфейса.
Я выделил из проекта только содержательную часть, которая сводится к одному классу: Internals.
Вот его код:

Код класса 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.
Поле inputs — массив из двух элементов, будет использоваться для генерации событий клавиатуры. Нет смысла выделять его динамически, если архитектура приложения предполагает, что вызова SendKey в несколько потоков не будет выполняться.
Собственно, дальше дело техники: заполняем соответствующие поля структур виртуальным кодом клавиши и отправляем в очередь ввода операционной системы.

SW3

Требование закрывается очень просто. Заводится флаг и ещё одна команда, которая обрабатывается особым образом: флаг переключается в противоположное логическое состояние. Если он установлен, то остальные команды игнорируются.

Вместо заключения

Улучшайзингом можно заниматься бесконечно, но это уже совсем другая история. Я не привожу здесь проект Windows-клиента, потому как он предоставляет широкий полёт фантазии.
Для управления медиаплеером посылаем один набор «нажатий» клавиш, если надо управлять презентациями — другой. Можно сделать модули управления, собирать их либо статически, либо в виде подключаемых плагинов. Вообще много чего можно. Главное — желание.

Спасибо за внимание.

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *