ИК пульт для ПК и розеток. Часть 2

в 14:18, , рубрики: ATmega168, DIY, diy или сделай сам, electronics, Электроника для начинающих

Часть 1
Вчера я рассказал, как собрать USB-IRPC на основе Arduino. Зачем? Чтобы показать, как быстро собрать макет и сложности особой тут нет.
Сегодня сделаем все как положено. С самого начала — с принципиальной схемы, PCB, ЛУТ. Кусочек фольгированного стеклотекстолита превратим с помощью кучки деталек в готовое устройство. Разумеется с корпусом, мы же хотим аккуратно, правда?
Вот наша цель:
USB-IRPC Bare Front
«USB-IRPC Bare Front»
USB-IRPC Finished
«USB-IRPC Finished»

Итак, начинаем с принципиальной схемы. На схеме будет только сам USB-IRPC, так как модуль розеток с реле и ИК приемник со светодиодом — это «периферия» и мы будем использовать ее без изменений.
Я использовал DipTrace для рисования схемы и печатной платы по следующим соображениям:

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

У него есть существенный недостаток — не очень богатая библиотека элементов, а также не самый удобный поиск по ней. Но редактировать элементы не слишком сложно. В общем, пока что это наименее неудобная для меня программа :)
Вот сама схема (смотреть лучше полноразмерную версию перейдя по ссылке и открыв в оригинальном размере):
USB-IRPC Scheme
«USB-IRPC Scheme»
В центре у нас U1 — я использовал Atmega168PA-AU, но подойдет и Atmega88 и Atmega328P. Размер прошивки всего чуть больше 4 кб.
Работу микроконтроллера (МК) на частоте 16МГц обеспечивают конденсаторы С4 и С5 с кварцем X1.
С2 — просто блокировочный конденсатор, немного сглаживает пульсации в цепи питания.
Блок из R3 и U2 служит для «нажатия» кнопки питания ПК.
J1 — разъем для подключения к материнской плате параллельно кнопке питания.
S1 — кнопка Reset. Может пригодиться при отладке.
J5 и J6 — разъем ISP6 для внутрисхемного программирования (прошивки).
J2 — разъем для подключения модуля розеток с реле.
J3 — разъем для подключения модуля ИК приемника со светодиодом.
D1 — светодиод, подключенный через токоограничивающий резистор R1, дублирует на плате выносной светодиод. Полезен при отладке, после сборки его не видно внутри коробки, поэтому впаивать его не обязательно. R4 — резистор для выносного светодиода на модуле приемника.
J4 — разъем USB Mini-B.
D2 и D3 — стабилитроны на 3.6В.
R5, R6, R7 — резисторы для обеспечения работы V-USB.
Вот, собственно и вся схема.
В конце статьи ссылки на скачивание ее в формате .sch (DipTrace)
Сложнее оказалось развести печатную плату. У меня это первый опыт и один их проводников в одном слое развести так и не удалось, пришлось оставить перемычку. К тому же я забыл, что вилки на плату будут устанавливаться с другой стороны и пришлось потом делать раздельную вилку для программатора. Оптопару я тоже по ошибке расположил не с той стороны, но запаять ее это не помешало. Все это я исправил в версии печатной платы, которую выложил для скачивания. Так что ваша плата будет чуть-чуть отличаться от той, что на моих фото. В лучшую сторону :)
В ходе запайки элементов я обнаружил, что заливка медью слишком близко к дорожкам добавляет неудобств и изменил заливку, избавившись от островков и сделав отступы от дорожек пошире.
Вот что получилось:
ИК пульт для ПК и розеток. Часть 2
Красная дорожка — перемычка, которую развести не удалось.
Приятный бонус DipTrace — возможность посмотреть как будет выглядеть печатная плата. К сожалению, в библиотеке есть далеко не все элементы, но общий резуьтат все равно куда более наглядный чем в плоском виде:
ИК пульт для ПК и розеток. Часть 2
Видно, что на плате я добавил разъем J7 просто еще пара штырьков, запараллеленных с J1, чтобы подключить коннектор кнопки питания с корпуса ПК.
Ок, схема и образец печатной платы готовы, закупаем детали и изготавливаем.

Список деталей:

U1 — Atmega168PA-AU — в корпусе TQFP-32 — от 64 рублей (в icdarom, минимальный заказ 1500р, доставка 200), штучно можно купить по 130 рублей свободно. Подойдет Atmega88PA-AU или Atmega328P-AU

Выводные элементы:
U2 — PC817, корпус PDIP4
S1 — DTS-61 Кнопка тактовая 6х6х4.3.
D2, D3 — стабилитроны на 3.6В
J2, J3 — PBS-4
J5, J6 — PBS-3
J1, J7 — PBS-2

SMD элементы размера 0805
R1, R4 — 390Ом
R5, R6 — 68Ом
R7 — 2.2 кОм
D1 — светодиод (любого цвета, у меня оранжевый)
J4 — USB miniB разъем
С1, С2 — конденсаторы 0.1 мкФ керамические.
С4, С5 — конденсаторы 22 пФ керамические
X1- кварц 16МГц
R3 — 100 Ом.
R2 — 10 кОм

Выводные элементы покупаются на радиорынке — рублей на 50 примерно.
SMD элементы я купил сразу пачку за $9.85 — там по 40 штук каждого номинала резисторов и по 30 каждого конденсатора.

Коробочку я брал в icdarom за 40р, но она есть и в других магазинах BOX-KA08 разного цвета, в районе 65 рублей.
Бывает даже прозрачные:
image

Учить вас делать платы ЛУТом не буду — DiHALT все описал просто и понятно. Кстати, поздравьте его с днем варенья. :)
Остановлюсь только на моментах, специфических для DipTrace. При печати не забудьте отзеркалить и отключите печать контуров компонентов:
ИК пульт для ПК и розеток. Часть 2
Можно обойтись и без DipTrace. Я экспортировал в PNG файл, берем FastStone Viewer и печатаем вот так:
ИК пульт для ПК и розеток. Часть 2
Качество печати на максимум, экономию отключить, разрешение на максимум.
PNG подготовлен в 600 PPI.
Дальше все как обычно: переводим, травим, отмываем тонер, выпиливаем, лудим, сверлим:
USB-IRPC
«USB-IRPC»
У меня бумага была матовая и тонер немного скатался по краям — может не прогладил как следует, но там ничего важного нет, пострадала только эстетика и то не сильно.
Запаиваем компоненты и наносим маркировку:
USB-IRPC Bare Front
«USB-IRPC Bare Front»
USB-IRPC Bare Back
«USB-IRPC Bare Back»
обратите внимание, у меня PC817 не с той стороны и ряды гребенки ISP разъема в обратном порядке — это моя ошибка, на вашей плате все будет правильно.
Из-за того, что маску в своем варианте сделал не очень удачно, при запайке элементов появились наплывы припоя, которые очень трудно снять совсем. Этот момент я тоже учел и исправил в окончательной версии разводки платы. Но переделывать плату не стал — работает хорошо, только выглядит не очень, к сожалению. Первый блин всегда комом. Зато залудить в этот раз удалось аккуратнее.

Сборка и оформление

Советую распечатать цветную картинку с надписями элементов в масштабе 1:1 (USB-IRPC_pcb.jpg) и использовать как шпаргралку при запайке элементов и сверловке отверстий.
Надфилем аккуратно выпиливаем отверстия в коробочке. Контуры отверстий намечаем на корпусе маркером или карандашом, прорезав канцелярским ножом их контуры в бумажном шаблоне и приложив шаблон к корпусу. Сбоку пропиливаем паз для miniUSB разъема. Вставляем плату и примериваем:
USB-IRPC
«USB-IRPC»
Если не планируете дорабатывать прошивку, то отверстия под ISP вырезать не обязательно — пины не упираются в крышку.
Затем печатаем и наклеиваем маркировку. В архиве найдете версию наклейки с ISP и без него. в зависимости от того, будете вы прорезать под них дырки или нет. (дырку под Reset я просверлил, но в наклейке прорезать не стал, ее можно проколоть зубочисткой, если понадобится, но пока не пригодилось).
ИК пульт для ПК и розеток. Часть 2
Все, аппаратная часть готова.
USB-IRPC Size
«USB-IRPC Size»
ИК модуль я просто залил термоклеем и на двусторонний скотч прикрепил к ножке монитора.
USB-IRPC IR-Led module
«USB-IRPC IR-Led module»
Я не люблю яркий свет дома, а при нормальном освещении его практически незаметно — размер 1х3 см.

Программируем девайс

Прошивка написана на С в Code::Blocks IDE. Компилятор — AVR GCC из комплекта AVR Toolchain
Я здесь поясню основные моменты, чтобы вы могли разобраться в коде проекта. Исходники лежат на странице проекта, ссылки в конце статьи.
Я написал имитацию функций DigitalRead и DigitalWrite, т.к. некоторые библиотеки беру из Arduino и чтобы не переписывать просто подсовываю им свой вариант вызываемых ими функций Wiring.
ir.h — это интерфейс с ИК приемником. Бессовестно адаптирован пример, который шел с китайским набором для экспериментов с ИК пультом. Практически без изменений.
usb_comm.h — определяет коды команд для обмена с ПК и структуру данных, в которой они передаются.

//USB Commands
#define cmdGetIRPCState 1 // Запрос состояния IRPC
#define cmdSetDigitalPinStates 2 // Установить состояния реле и выносного светодиода - битовое поле
#define cmdDisableIR 3 // Запретить обработку нажатий кнопок ИК пульта
#define cmdEnableIR 4 // Разрешить обработку нажатий кнопок ИК пульта
#define cmdGetIRBtnMapping 5 // Получить привязку внутренней функции к коду кнопки пульта
#define cmdSetIRBtnMapping 6 // Установить привязку внутренней функции к коду кнопки пульта
#define cmdGetIRBtn 7 //  Считать код последней нажатой кнопки пульта (на данный момент уже не используется)
#define cmdSetLedState 8  // Вкл/выкл светодиод
#define cmdDoInternalFunc 9 // выполнить внутреннюю функцию IRPC
#define cmdSaveToEEPROM 10 // сохранить состояние реле и светодиода в EEPROM 
#define cmdLoadFromEEPROM 11 // загрузить состояние реле и светодиода из EEPROM 

//USB pinstate bits
#define PWRRelay1Bit 0
#define PWRRelay2Bit 1
#define ledBit 7


//Internal functions
#define fRelay1Switch 1
#define fRelay2Switch 2
#define fPCPwrSwitch  3

struct irpcdata_t       // Описание структуры для передачи данных
{
   uint8_t cmd;      // Switches state
   uint8_t data[4];        // data
};

USB pinstate bits — номера битов управления реле и светодиодом в пакете.
Внутренние функции IRPC:
fRelay1Switch — переключить реле 1
fRelay2Switch — переключить реле 2
fPCPwrSwitch — нажать кнопку питания ПК

irpcdata_t — это структура пакета, которым обмениваются устройство и ПК.
Обмен происходит по запросу ПК.
cmd — код команды (например cmdGetIRPCState)
data[] — массив данных из 4 байт. Каждая команда интерпретирует его по своему.

irrc.h — соответствие кодов кнопк названиям, просто для удобства. Я записал для своих пультов. Примерно так:

//AverMedia RM-FR
#define amTVFM 		0xFE01
#define amPWR 		0xFF00
#define amB1 		0xFA05
#define amB2 		0xF906
#define amB3 		0xF807
#define amB4 		0xF609

В файле usbconfig.h настройки библиотеки V-USB. Я черпал информацию из этой статьи.
Если вы не разбираетесь в USB протоколе, то там можно ничего не трогать.
Основаная программа main.c

Определяем макрос SetPinState, который занимается установкой выходов в требуемое положение и устанавливает соответствующее состояние бит этого выхода в переменной pinStates.
Также определяем пины, к которым подключена периферия и количество соответствий кнопок внутренним функциям, которые мы можем хранить (MaxBtnMapings).

//Usefull macroses
#define SetPinState(Pin, State, bit) digitalWrite(Pin, State);  if (State>0) pinStates |=_BV(bit); else pinStates &= ~_BV(bit);

//PORTB
#define PWRPin 16   // PC2
#define ledPin 9    // PB1

//PORTD
#define PWRRelay1 6  // PD6
#define PWRRelay2 5  // PD5

#define MaxBtnMapings 15

Переменные (комментарии в исходнике делал для себя, поэтому в файле они в основном на корявом английском :)

//Состояние
uint8_t ledState = 0;         // состояние светодиода

uint8_t PWRRelay1State = 1; // Состояние реле 1
uint8_t PWRRelay2State = 1; // Состояние реле2 
uint8_t PWRPinState=0;       // состояние кнопки питания ПК
uint8_t IRBtnL, IRBtnH;         // Переменные, хранящие для отправки ПК код кнопки пульта
uint8_t IREnabled=1;         // Декодировать ли ИК коды

struct btn_mapping btn_mappings[MaxBtnMapings]; // мэппинг кнопок внутренним функциям

//--
uint8_t cmd;            // Полученная от команда
uint8_t pinStates=0x00; //Состояния управляемых пинов побитово (0-PWRRelay1State, 1-PWRRelay2State)
uint8_t cmdReceived=0;  //Команда от PC получена (флаг)

//IR variables
uint16_t btn;	// код кнопки пульта
uint16_t LastBtn=0x00;  // последняя нажатая кнопка пульта
//----------------- USB Section---------------------------

struct irpcdata_t pdata, pcdata;  //pdata - output buffer, pcdata - input buffer

Принимаемые команды от ПК храним в pcdata, отправляемые — в pdata.

Основной цикл

int main(void)
{
    //Initialize ports
    DDRB = 0b00000010;      // PB1 - выход
    DDRC = 0b00000100;      // PC2 - выход
    DDRD = 0b01100000;      // PD5, PD6 - выход

    //clear btn mappings
    for (i=0; i<MaxBtnMapings;i++){
      btn_mappings[i].func=0;
      btn_mappings[i].btn_code=0;
    }

    loadStateFromEEPROM();


    //USB Init & connect
  	cli(); // Clear interrupts while perfoorming time-critical operations
    usbInit();
    usbDeviceDisconnect();  // принудительно отключаемся от хоста, так делать можно только при выключенных прерываниях!

    uchar i = 0;
    while(--i){             // пауза > 250 ms
        _delay_ms(1);
    }

    usbDeviceConnect();     // подключаемся
    sei();                  // разрешаем прерывания

    //Timer1 Init for IR
    timer1_init();          // init IR timer

    loop();
    return 0;
}

Настраиваем пины, к которым подключены реле и светодиод как выходы.
Очищаем соответствия кнопок внутренним функциям — мало ли какой мусор в памяти при включени.
Загружаем байт состояний реле и светодиода — pinStates и устанавливаем их в соответствующее состояние в функции loadStateFromEEPROM().
Отключаем прерывания, отключаемся от ПК, делаем паузу и даем нас обнаружить. Затем разрешаем прерывания.
Подготавливаем таймер для работы с ИК приемником.
Запускаем цикл обработки данных от ИК приемника и ПК:

void loop()
{
    // Main loop
    for(;;){                // главный цикл программы
      usbPoll();            // эту функцию надо регулярно вызывать с главного цикла, максимальная задержка между вызовами - 50 ms
      if (cmdReceived) {processUSBcmd();}
       else if (IREnabled) {
        remote_decode();
        process_IR();
       }

    }
    return;
}

Тут все просто:

  • Интересуемся у ПК, не было ли чего для нас.
  • Если была принята команда, обрабатываем ее в processUSBcmd()
  • Дальше если разрешено обрабатывать коды ИК пульта, вызываем функцию прием и декодирования — это самая длинная по времени выполнения функция.
  • Обрабатываем код нажатой кнопки и записываем его для передачи ПК когда он спросит.

Когда ПК хочет передать нам пакет, вызывается функция usbFunctionWrite, когда он запрашивает пакет данных — функция usbFunctionRead.

Разберем их:

uchar   usbFunctionRead(uchar *data, uchar len)
{
    if(len > bytesRemaining)
        len = bytesRemaining;

    uchar *buffer = (uchar*)&pdata;

    if(!currentAddress)        // Ни один кусок данных еще не прочитан.
    {                          // Заполним структуру для передачи
      pdata.cmd=cmd; //last received cmd
      switch(cmd){
        case cmdGetIRPCState:
          pdata.data[0]=pinStates;
          pdata.data[1]=IRBtnH;
          pdata.data[2]=IRBtnL;
          if (IREnabled){
            //clear IR button code
            IRBtnL=0;
            IRBtnH=0;
          }
        break;
        case cmdGetIRBtnMapping:
          pdata.data[0]=pcdata.data[0];
          pdata.data[1]=(btn_mappings[pcdata.data[0]].btn_code>>8);
          pdata.data[2]=(btn_mappings[pcdata.data[0]].btn_code & 0xFF);
          pdata.data[3]=btn_mappings[pcdata.data[0]].func;
        break;
      }
    }

    uchar j;
    for(j=0; j<len; j++)
        data[j] = buffer[j+currentAddress];

    currentAddress += len;
    bytesRemaining -= len;
    return len;
}

Сначала заполняем структуру pdata — если нас спросили состояние девайса (cmdGetIRPCState), записываем pinStates и код нажатой кнопки пульта
Если запросили одно из соответствий кнопок внутренним функциям — отдаем его, взяв номер функции из первого байта присланного ПК пакета (pcdata.data[0])
Если команду не узнали, просто отдаем структуру pdata — при обработке команд результаты записываются в нее.
Затем побайтово записываем наш пакет по указателю, переданному в параметре data.

uchar   usbFunctionWrite(uchar *data, uchar len)
{
    uchar *buffer = (uchar*)&pcdata;
    uchar j;
    for(j=0; j<len; j++)
        buffer[j]=data[j];

    cmdReceived=1;        // Выставим флаг принятых данных

    return 1;
}

Тут еще проще — просто переписываем пакет с данными в pcdata, не разбираясь что к чему, обработка будет выполняться processUSBcmd(), а нам нужно вернуть управление как можно быстрее — функция вызывает прерыванием.

А вот, собственно, и она:

void processUSBcmd(){
  cmd=pcdata.cmd;

  switch(cmd)
  {
    case cmdSetDigitalPinStates:
      //Get pin states from USB cmd
      PWRRelay1State=(pcdata.data[0] & _BV(PWRRelay1Bit));
      PWRRelay2State=(pcdata.data[0] & _BV(PWRRelay2Bit));
      ledState=(pcdata.data[0] & _BV(ledBit));
      //Execute cmd
      SetPinState(PWRRelay1, PWRRelay1State, PWRRelay1Bit);
      SetPinState(PWRRelay2, PWRRelay2State, PWRRelay2Bit);
      break;
    case cmdGetIRPCState:
      if (PWRRelay1State>0) pinStates |=_BV(PWRRelay1Bit); else pinStates &= ~_BV(PWRRelay1Bit);
      if (PWRRelay2State>0) pinStates |=_BV(PWRRelay2Bit); else pinStates &= ~_BV(PWRRelay2Bit);
      if (ledState>0) pinStates |=_BV(ledBit); else pinStates &= ~_BV(ledBit);
      break;
    case cmdGetIRBtn:
      pdata.data[1]=IRBtnH;
      pdata.data[2]=IRBtnL;
      IRBtnL=0;
      IRBtnH=0;
      break;
    case cmdSetLedState:
      ledState=(pcdata.data[0] & _BV(ledBit));
      SetPinState(ledPin, ledState, ledBit);
      break;
    case cmdEnableIR:
      IREnabled=1;
      break;
    case cmdDisableIR:
      IREnabled=0;
      IRBtnL=0xFF;
      IRBtnH=0xFF;
      break;
    case cmdSetIRBtnMapping:
      btn_mappings[pcdata.data[0]].btn_code=(uint16_t)(pcdata.data[1]<<8)+pcdata.data[2];
      btn_mappings[pcdata.data[0]].func=pcdata.data[3];
      break;
    case cmdDoInternalFunc:
      switch (pcdata.data[0]){
        case fRelay1Switch:
          PWRRelay1State=!PWRRelay1State;
          SetPinState(PWRRelay1, PWRRelay1State, PWRRelay1Bit);
          break;
        case fRelay2Switch:
          PWRRelay2State=!PWRRelay2State;
          SetPinState(PWRRelay2, PWRRelay2State, PWRRelay2Bit);
          break;
        case fPCPwrSwitch:
          digitalWrite(PWRPin, 1);
          _delay_ms(100);
          digitalWrite(PWRPin, 0);
          break;
      }
      break;
    case cmdLoadFromEEPROM:
      loadStateFromEEPROM();
      break;
    case cmdSaveToEEPROM:
      saveStatetoEEPROM();
      break;
  }
  _delay_ms(5);
  cmdReceived=0;
}

В зависимости от принятой команды (ее код в поле pcdata.cmd) разбираем пакет pcdata.data
тут все довольно прозрачно, я думаю.
Выполнив команду, сбрасываем флаг cmdReceived.

В функции process_IR() мы собираем 16 битный код кнопки:

    // button code is 16 bit
    btn=(adrH_code << 8) + adrL_code;

Если код не равен 0, значит что-то нажато было, сохраним для передачи ПК и выставим флаг, что нужно зажечь светодиод — показать, что мы увидели нажатую кнопку.

    //Button pressed
    if (btn>0x00) {
      LastBtn=btn;
      IRBtnL=adrL_code;
      IRBtnH=adrH_code;
      ledState=1;
    }

дальше я обрабатываю коды кнопок своего пульта:

    //Button is "Power"
    if ((btn==ykPWR) || (btn==amPWR)) {
      PWRPinState=1;
    }

      switch (LastBtn){
//        case ykB1:
        case amSNAPSHOT:
          PWRRelay1State=!PWRRelay1State;
          SetPinState(PWRRelay1, PWRRelay1State, PWRRelay1Bit);
          break;
//        case ykB2:
        case am16CH:
          PWRRelay2State=!PWRRelay2State;
          SetPinState(PWRRelay2, PWRRelay2State, PWRRelay2Bit);
          break;
    }

По кнопке «Power» нажимаю кнопку питания ПК до тех пор пока не отпустят кнопку пульта.
По двум другим кнопкам переключаю состояния реле.

Затем проверяем массив назначенных соответствий кодов кнопок внутренним функциям — это для использования с другими пультами, соответствия задаются с ПК командами cmdSetIRBtnMapping — по одной команде на кнопку. Обработку этой команды мы видели выше в processUSBcmd()

      for (i=0; i<MaxBtnMapings;i++){
        if (btn==btn_mappings[i].btn_code) {
          switch (btn_mappings[1].func){
            case fRelay1Switch:
              PWRRelay1State=!PWRRelay1State;
              SetPinState(PWRRelay1, PWRRelay1State, PWRRelay1Bit);
              break;
            case fRelay2Switch:
              PWRRelay2State=!PWRRelay2State;
              SetPinState(PWRRelay2, PWRRelay2State, PWRRelay2Bit);
              break;
            case fPCPwrSwitch:
              digitalWrite(PWRPin, 1);
              _delay_ms(100);
              digitalWrite(PWRPin, 0);
              break;
          }
        }
      }

В целом — главное успевать быстро обрабатывать прерывания и периодически опрашивать ИК приемник и вызывать usbPoll, иначе ПК нас потеряет — «USB device not recognized».
Версия обозначена как альфа недаром — есть несколько кусков, как вы успели заметить, где привязка к конкретному пульту пока не закоментирована, потому что настройка мэппинга кнопок и функций с ПК еще не написана, не успел. Ну и иногда бывает, что девайс перестает принимать команды от ПК, хотя в системе виден и на кнопки пульта реагирует нормально. Подозреваю, что это связано с большой длительностью опроса ИК приемника, это нужно оптимизировать. Однако, бывает это редко и не доставляет особых неудобств.

Теперь переходим к программе для ПК.
Остановлюсь на ключевых моментах, чтобы можно было разобраться в исходниках.
Покажу на примере плагина для MKey — в нем проще разобраться.
Плагин написан на Delphi7 потому что выяснилось, что MKey не поддерживает юникод, а в Delphi 2010 с ANSI нужно дополнительно возиться.
Из дополнительных библиотек используется только JVCL.

Основная работа выполняется в модуле uHID.pas
В нем определяются те же константы команд, что и в девайсе, не буду их дублировать.
Та же запись, описывающая пакет, с одним добавлением — reportID — мы его не поддерживаем и там всегда 0, но вообще репортов у девайса может быть не один. Просто нам хватает и одного.
Дополнительные константы только id девайса — Vendor ID (Vid), Product ID (Pid) и DeviceName. По Vid и Pid мы будем искать среди подключенных HID устройств наше.

const
  //USB-IRPC IDs
  Vid=$16C0;
  Pid=$05DF;
  DeviceName='USB-IRPC';

  //PC<->IRPC
  TIRPCReport= packed record
    reportID:byte; //not used, shoud be 0
    cmd:byte;
    data: array [0..3] of byte;
  end;

Все функции общения с девайсом собраны в класс

 TIRPC=class
  private
    Dev:TJvHidDevice;
    HID:TJvHidDeviceController;
    fIRButtons:TStringList;
    fIRName:string;
    fIRLoaded:boolean;
    function IsConnected: boolean;
  public
    constructor Create;
    destructor Destroy; override;

    procedure Connect;
    procedure Disconnect;

    function  ReadIRPCState(var Report:TIRPCReport):boolean;
    procedure WriteIRPCcmd(cmd:TIRPCReport);

    procedure LoadIRcodes(irFile:string);

    function BtnName(btnCode:word):string;overload;
    function BtnName(btnCode:string):string;overload;


    property Connected:boolean read IsConnected;
    property Device:TJvHidDevice read Dev;
    property IRButtons:TStringList read fIRButtons;
    property IRName:string read fIRName;
  end;

поле Dev — указатель на класс устройства, мы его получаем при удачном подключении к устройству (Connect).
при отключении (Disconnect) он устанавливается в nil.

Методы очень простые, поэтому покажу одну для примера, остальные примерно того же порядка сложности:

procedure TIRPC.Connect;
begin
  if Assigned(dev) then Exit;

  HID.CheckOutByID(Dev,VID,Pid);
end;

состояние подключения (свойство Connected) определяется указателем Dev — если он не пустой, значит подключение прошло успешно.

Метод WriteIRPCcmd(cmd:TIRPCReport) отправляет пакет с командой, которую вы предварительно заполняете, девайсу.
Метод ReadIRPCState(var Report:TIRPCReport):boolean запаршивает пакет у девайса. Если пакет успешно получил, вернет true, а сам пакет запишет в параметр Report.

procedure LoadIRcodes(irFile:string); — загружает файл соответствий названий кнопок пульта их кодам. Файл имеет простой формат: каждая строка содержит через запятую название кнопки и код
TVFM,$FE01
В плагине этот метод не востребован — в Mkey передается код кнопки, вы сами называете ее как хотите, потому что программа не показывает что вы ей отправили. А вот в своей программе можно показывать нормальные человеческие названия кнопок.
Функции BtnName возвращают название кнопки по коду, переданному в виде 16 битноо значения или строки с хексом.
function BtnName(btnCode:word):string;overload;
function BtnName(btnCode:string):string;overload;
свойство IRButtons содержит список кнопок и их кодов
property IRButtons:TStringList read fIRButtons;
свойство IRname — имя загруженного методом LoadIRcodes пульта (имя файла без расширения .ir).
property IRName:string read fIRName;

Как получить пакет от девайса?

function TIRPC.ReadIRPCState;
begin
  Result:=false;
  if not Assigned(dev) then Exit;
  Result:=dev.GetFeature(report,sizeof(TIRPCReport){Dev.Caps.FeatureReportByteLength});
end;

С помощью метода GetFeature — передаем буфер для пакета, получаем в него пакет. Если подключения нет, метод вернет False.
Передача пакета девайсу производится аналогично:

  dev.SetFeature(cmd,SizeOf(cmd));

Собственно сам плагин выполнен в виде dll.
Реализованы требуемые им функции:
Enable, Disable, GetName, GetInfo, GetVer, GetAuthor, IsInteractive, IsHaveSettings;
Соответственно в Enable мы подключаемся к девайсу методом Connect, запускаем таймер, который будет каждые 500 мс опрашивать девайс с помощью ReadState и если получил код кнопки, отдавать его в MKey:

function Enable: boolean; stdcall;
begin
  IRPCPlugin:=TIrPCPlugin.Create;
  IRPCPlugin.IRPC.Connect;

  tmrReadState:=TTimer.Create(nil);
  tmrReadState.Interval:=500;
  tmrReadState.OnTimer:=IRPCPlugin.ReadState;
  tmrReadState.Enabled:=true;

  result:=true;
end;
procedure TIRPCPlugin.ReadState(Sender: TObject);
var
  mainWnd:hWnd;
  pluginact:string;
  Btn:Word;
begin
  IRPCPlugin.IRPC.Connect;
  If not IRPCPlugin.IRPC.Connected then MessageBox(0,'IRPC device not connected', 'IRPC', MB_ICONEXCLAMATION);

  if rep.cmd<>cmdGetIRPCState then begin
    rep.cmd:=cmdGetIRPCState;
    IRPCPlugin.IRPC.WriteIRPCcmd(rep);
  end;

  IRPCPlugin.IRPC.ReadIRPCState(rep);

  Btn:=rep.data[1] shl 8+rep.data[2];

  If btn<>0 then begin
    pluginact:=Format('%x',[Btn]);
    mainwnd:=Findwindow('TMainForm', 'MKey');
    SendMessage(mainwnd, WM_PLUGIN, plugin_interface_version, DWORD(PChar(pluginact)));
  end;
end;

В процедуре Disable отключаемся от девайса:

function Disable: boolean; stdcall;
begin
  IRPCPlugin.IRPC.Disconnect;
  FreeAndNil(IRPCPlugin);
  FreeAndNil(tmrReadState);
  result:=true;
end;

Модуль uHID у плагина и полноценной программы управления идентичный.
Основную программу выложил пока в скомпилированном виде, исходники не выкладываю, потому что над функционалом идет работа, там пока бардак :) Как будет более-менее приличный вариант, в Downloads положу.
Пока выглядит вот так:
image

  • Сворачивается в область уведомлений.
  • По клику разворачивается.
  • Период опроса настраивается в Settings
  • S — сохранение состояния в EEPROM
  • L — загрузка состояния из EEPROM
  • Power Btn — выполняет функцию fPCPwrSwitch (то бишь нажимает кнопку питания на ПК)
  • Show log покажет список событий — засыпание / просыпание ПК. (у меня кнопка питания отправляет в Sleep).
  • Остальное — тестовые кнопки.
  • Кликая по одной из трех лампочек, переключаем соответственно — светодиод, Реле1, Реле2 (всплывающие подсказки помогут разобраться)

Вроде все важное рассказал. Если остались вопросы, задавайте в комментариях, постараюсь ответить.

Исходники

Автор: RaJa

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js