Как стать автором
Обновить

Управление компьютером через ПДУ от усилителя с помощью Arduino и Node.js

Время на прочтение 5 мин
Количество просмотров 11K

Предисловие


История начинается с того, что пол года назад я купил усилитель Yamaha A-S501.


Yamaha A-S501


В комплекте с ним шёл пульт дистанционного управления, который мог управлять и усилителем, и ямаховским CD-плеером, которого у меня естественно не было. Поэтому большинство кнопок на пульте попросту не использовались. Да и в целом в самом пульте не было необходимости, и он всегда лежал на полке.


Однако глядя на него, мне не давала покоя мысль задействовать пульт на полную катушку. Например, было бы удобно лёжа на диване и смотря фильм, быстрым движением руки перемотать, поставить его на паузу и т.д. Конечно, для этих целей я раньше использовал приложения на смартфоне для управления программами MPC-HC, Foobar2000, но пультом было бы быстрее и удобнее.


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


И так, поехали...


Готовые решения


Один из существующих аналогов, который я смог найти, — это Flirc. С помощью него можно эмулировать нажатия клавиш на физической клавиатуре компьютера.



Такой ИК-приемник стоит здесь 100 злотых (≈$28). Забегая вперед, это вдвое дороже того, что у меня вышло. К тому же, по функциональности у меня получилось даже лучше (субъективно).


Покупка деталей



Мне понадобилось:


  • Собственно, сама плата Arduino Uno. Стоит заметить это не оригинальная плата, а какой-то польский клон. По описанию — она полностью аналогична оригиналу. (27,90 zł)
  • Инфракрасный приёмник VS1838B HX1838 (напряжение: 3,3–5 V, частота: 38 kHz, угол: 90°) (1,30 zł)
  • Плата для прототипирования + провода (13,90 zł)
  • Пустая плата, чтобы всё спаять (2,10 zł)
  • Коннекторы для соединения плат (2,51 zł)

Итого: 47,71 zł (≈$14)


Программное обеспечение


Пока ждал доставку я начал писать "драйвер", который должен считывать данные из последовательного порта от Arduino и выполнять определённые действия для нажатой кнопке на пульте.


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


  • Эмуляция нажатия клавиши на клавиатуре (через node-key-sender):

{ "key": "space" }

  • Запуск произвольной программы с параметрами:

{ "exec": ["c:\\Program Files (x86)\\foobar2000\\foobar2000.exe", "/play"] }

  • Условие (используется ps-list):

{ "if": { "running": "mpc-hc.exe" }, "then": [ ... ], "else": [ ... ] }

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


const runHandlers = require('./handlers')

module.exports = async function run(actions) {
  if (!Array.isArray(actions)) {
    actions = [actions]
  }
  for (const act of actions) {
    await runHandlers(act)
  }
}

Вместо тысячи слов документации всё расскажут тесты:


run
  when "exec" action
    √ executes the specified file without args (as array) (4ms)
    √ executes the specified file without args (as string) (1ms)
    √ executes the specified file with args
    √ rejects if "exec" has wrong type (5ms)
  when "key" action
    √ sends the specified key press if passed string (1ms)
    √ sends the specified key combination if passed array
    √ rejects if "key" has wrong type (1ms)
  when "if" action
    √ rejects if no "then" (1ms)
    √ rejects if operator is not supported
    when operator if "running"
      √ runs "then" actions if the condition is true (1ms)
      √ runs "else" actions if the condition is false
      √ does not run anything if the condition is false and no "else" statement (1ms)
  when multiple actions
    √ executes all actions (1ms)
  when multiple actions are mixed into one
    √ runs only first one alphabetically

Осталось дождаться заветных деталек.


Железо


Признаюсь, я не изобретал ничего нового, всё уже давно было сделано до меня. Я просто воспользовался готовой схемой из статьи How to Set Up an IR Remote and Receiver on an Arduino.


Схема довольна проста:



На практике:




Прошивка


Прошивку я также честно позаимствовал из статьи, для её работы понадобится IRremote Arduino Library.


Коды кнопок заменил на актуальные от моего пульта:


void loop() {
  if (irrecv.decode(&results)) {
    if (results.value == 0xFFFFFFFF) {
      results.value = key_value;
    }

    switch (results.value) {
    case 0x9E6140BF:
      Serial.println("play");
      break;
    case 0x9E61AA55:
      Serial.println("pause");
      break;
    /* ...*/
    case 0x5EA1A857:
      Serial.println("cd");
      break;
    default:
      Serial.println(results.value, HEX);
      break;
    }
    key_value = results.value;
    irrecv.resume();
  }
}


Как только в окошке Монитора порта в Arduino IDE появились названия нажатых кнопок необходимо было добавить в драйвер компонент для работы с последовательным портом.


Получилась обёртка над библиотекой serialport и, собственно, потоком данных из порта:


const SerialPort = require('serialport')

module.exports = class SerialPortReader {
  constructor(port) {
    const serialPort = new SerialPort(port)
    this.lineStream = serialPort.pipe(new SerialPort.parsers.Readline())
  }

  start(handler) {
    this.lineStream.on('readable', () => {
      const data = this.lineStream.read().trim()
      handler(data)
    })
  }
}

Позже появилась необходимость превратить обработчики в "debounce" функцию, т.к из пульта поступает быстро повторяющийся сигнал, который даже при кратковременном нажатии на кнопку успевает отправиться несколько раз. Однако убирать такую опцию для всех кнопок тоже не совсем уместно, например, для громкости.


Финальный код выглядит так:


const debounce = require('debounce')
const settings = require('./lib/settings')
const run = require('./lib/run')
const SerialPortReader = require('./lib/SerialPortReader')

const simpleHandle = async button => {
  const actions = settings.mappings[button]
  if (!actions) {
    console.warn(`Action not found for remote control button "${button}"`)
    return
  }

  try {
    await run(actions)
  } catch (e) {
    console.error(e.message)
    process.exit(1)
  }
}

const debouncedHandle = debounce(simpleHandle, settings.debounceDelay, true)

const callHandleFn = button => {
  return (settings.noDebounce.includes(button) ? simpleHandle : debouncedHandle)(button)
}

const reader = new SerialPortReader(settings.serialPort)
reader.start(callHandleFn)

Создание независимой платы


Убедившись, что прототип работает, я приступил к созданию платы. Стоит отметить, для меня это первый опыт в подобных делах. У меня и паяльника-то подходящего не было с маленькой иглой — только старый советский большой с тугим проводом.


С горем пополам мне удалось припаять "ножки" (из двух больших коннекторов по 8 пин уцелело только 2 пина). Со всем остальным уже было попроще.



(Кривовато. Скорее всего из-за клона Arduino. Гнёзда стоят неровно относительно друг друга.)



Я намеренно поместил инфракрасный приёмник вовнутрь между платами. Так устройство с лёгкостью помещается под усилителем. Дорожки решил сделать, соединяя оловом отверстия на плате, вместо соединения проводами. И наконец, наклеил скотч наверх, чтобы плата не замыкалась об металлический корпус усилителя.




В итоге: полностью рабочий девайс и программное обеспечение за ≈$14. Полученный опыт и радость от проделанной работы и результата — бесценно! :-)


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




Демо:



Исходники на Гитхабе.




P.S. Спасибо ramanchik'у за консультацию :)

Теги:
Хабы:
+16
Комментарии 17
Комментарии Комментарии 17

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн