Управление самодельным устройством USB HID из Android KitKat Печать
Добавил(а) eska2000   

В моем проекте нужно было управлять реле из программы на устройстве с OS Android 4.4.2 (плеер Tronsmart VEGA S89 [1]), и также нужно отслеживать замыкание на выходе датчика цепи системы безопасности, и оповещать от этом пользователя (любая современная система пожарной безопасности имеет у себя на борту такие сухие контакты для передачи информации о пожаре на сторонние устройства).

Tronsmart-VEGA-S89

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

На корпусе Tronsmart VEGA S89 есть несколько портов USB, через которые и хотелось управлять релюшками. Разумеется, в первую очередь я подумал об Ардуино. И это действительно вполне подходящее решение, потому что написано множество статей на тему "как сделать что угодно на Arduino" - бери и пользуйся. Однако случайно наткнулся на макетную плату AVR-USB162 [2], построенную на микроконтроллере AT90USB162. Заинтересовало наличие аппаратной поддержки USB на плате AVR-USB162, простота схемы и разумеется цена - намного ниже, чем у стандартного Ардуино. Плата AVR-USB162 была заказана и через несколько дней получена.

at90usb162-04IMG_8330.jpg

Примечание: мои знания в сфере микроконтроллеров до этого времени были равны абсолютному нулю. Знания в сфере ассемблера так же ноль. СИ на уровне Hello world. Было несколько опытов программирования с Raspberry Pi на Python. И пользовался ActionScript для своих нужд. Поэтому всё-всё описанное далее не стоит рассматривать как инструкцию. Может быть, это будет интересно подобным начинателям, но не более.

[Программное обеспечение для микроконтроллера (firmware)]

Плату AVR-USB162 получил, начал разбираться. Чтобы можно было наладить связь через USB с микроконтроллером, в него должно быть записано firmware, которое работает как какое-то стандартное устройство USB. Самый популярный класс стандартных устройств USB - класс USB HID, с помощью которых решается большинство задач ввода и вывода.

Не сразу до меня дошло, какие примеры и какие библиотеки для программирования микроконтроллера надо использовать, по ошибке сначала начал с библиотеки V-USB. Оказалось, что это совсем не то, эта библиотека для "обычных" AVR, без аппаратного интерфейса USB. Для моего AT90USB162, у которого на борту аппаратная поддержка USB, подойдут примеры готового кода из библиотеки LUFA [3], там есть примеры реализации устройств USB HID.

Я скачал последнюю версию пакета LUFA с сайта автора [4], и нашел там нужные мне примеры кода на языке C (взял пример из папки Demos/Device/ClassDriver/GenericHID). Последняя версия LUFA 140928 имеет документацию на английском языке (комментарии в коде, DoxyGen), но в сети можно найти перевод на русский язык версии 101122 (см. [3], или прогуглите Библиотека LUFA 101122). Примеры кода из библиотеки LUFA компилируются запуском из командной строки команд make, поэтому нужно установить тулчейн AVR.

Примечание: тулчейн - это различные библиотеки для AVR, заголовочные файлы, компилятор языка C и ассемблера. В среде Windows можно использовать как тулчейн пакет WinAVR, или можно использовать тулчейн, входящий в состав установки Atmel Studio (на Linux, FreeBSD и других UNIX-подобных системах нужно установить тулчейн AVR-libc).

Я скачал и установил WinAVR (инсталлятор можно легко найти на сайте sourceforge.net, прогуглите Download WinAVR), попробовал скомпилировать пример Demos/Device/ClassDriver/GenericHID. Пример GenericHID, как и все проекты LUFA, компилируется командами make clean и make hex, выполненными из каталога проекта:

make clean
make hex

В результате в папке проекта GenericHID появится файл GenericHID.hex. Это прошивка firmware, который нужно прошить в память микроконтроллера либо с помощью программатора, либо с помощью USB загрузчика Flip (я пользовался Flip). Я прошил свой микроконтроллер через USB, но устройство USB HID у меня не заработало. Оказывается, нужно правильно отредактировать опции в файле makefile проекта, чтобы они соответствовали типу макетной платы, типу микроконтроллера и действительной рабочей тактовой частоте (опции MCU, BOARD, F_CPU). Вот содержимое файла makefile, которое должно соответствовать макетной плате AVR-USB162 (старые строчки опций MCU, BOARD, F_CPU я закомментировал символом #, и исправленные опции для наглядности выделил здесь жирным шрифтом):

# Выполните "make help" для получения подробной подсказки.
 
#MCU = at90usb1287
MCU = at90usb162
ARCH = AVR8
#BOARD = USBKEY
BOARD = MICROSIN162
#F_CPU = 8000000
F_CPU = 16000000
F_USB = $(F_CPU)
OPTIMIZATION = s
TARGET = GenericHID
SRC = $(TARGET).c Descriptors.c $(LUFA_SRC_USB) $(LUFA_SRC_USBCLASS)
LUFA_PATH = ../../../../LUFA
CC_FLAGS = -DUSE_LUFA_CONFIG_HEADER -IConfig/
LD_FLAGS =
 
# Цель по умолчанию
all:
 
# Include LUFA build script makefiles
include $(LUFA_PATH)/Build/lufa_core.mk
include $(LUFA_PATH)/Build/lufa_sources.mk
include $(LUFA_PATH)/Build/lufa_build.mk
include $(LUFA_PATH)/Build/lufa_cppcheck.mk
include $(LUFA_PATH)/Build/lufa_doxygen.mk
include $(LUFA_PATH)/Build/lufa_dfu.mk
include $(LUFA_PATH)/Build/lufa_hid.mk
include $(LUFA_PATH)/Build/lufa_avrdude.mk
include $(LUFA_PATH)/Build/lufa_atprogram.mk

Опции MCU, BOARD, F_CPU нужно вводить именно так, как указано, с соблюдением регистра символов. Теперь заново перекомпилируйте проект командами make clean, make hex, прошейте полученную прошивку в память микроконтроллера, и оно определится в компьютере как настоящее устройство USB HID!

Это параметры для управления препроцессором компилятора AVR GCC, которые встречаются в коде проектов LUFA.

MCU. Этот параметр показывает, какая целевая модель микроконтроллера должна использоваться в проекте (компилируемое приложение для микроконтроллера). Опцию MCU нужно установить в одно из возможных значений, поддерживаемых тулчейном (опция MCU управляет подключением заголовочных файлов в папке avr\include\avr тулчейна). В зависимости от того, какой микроконтроллер используется, в опции после знака = нужно указать конкретное значение - например для микроконтроллера AT90USB1287 нужно указать MCU = at90usb1287). Для макетной платы AVR-USB162 нужно указать MCU = at90usb162. Внимание: после знака = значение опции нужно указывать маленькими буквами!

Не все демонстрационные проекты поддерживают все типы микроконтроллеров Atmel AVR USB, поэтому см. список поддерживаемых архитектур в разделе Demo Compatibility файла описания проекта (например, для проекта GenericHID это файл описания GenericHID.txt).

BOARD. Эта опция показывает, под какое аппаратное устройство (макетную плату) будет скомпилирован проект. Некоторые куски кода библиотеки LUFA и примеров кода устройств зависят от аппаратуры макетной платы - чаще всего это касается управления индикационными светодиодами и опроса кнопок. Если Вы используете одну из поддерживаемых в LUFA макетных плат, то Вам нужно задать опцию BOARD в конкретное значение, соответствующее Вашей плате. Все возможные варианты для опции BOARD можно подсмотреть в файле LUFA\Drivers\Board\Board.h библиотеки LUFA.

Макетная плата AVR-USB162 также поддерживается LUFA, для неё надо указать значение опции BOARD = MICROSIN162.

F_CPU. Эта опция отражает реальную тактовую частоту, на которой работает микроконтроллер, частота указана в Гц. Эта опция также используется препроцессором как в модулях тулчейна, так и в коде примеров самой библиотеки LUFA. Опция F_CPU позволяет корректировать поведение кода в плане корректного отслеживания реального времени, правильно задавать настройки аппаратуры.

Для микроконтроллера AT90USB162 для поддержки USB возможны только 2 варианта выбора тактовой частоты: 8 МГц или 16 МГц. На моей макетной плате AVR-USB162 был установлен кварц на 16 МГц, поэтому надо указать значение опции F_CPU = 16000000.

В память микроконтроллера AT90USB162 уже записан USB-загрузчик, который работает вместе специальным программным обеспечением FLIP от компании Atmel. Поэтому нужно просто скачать с сайта Atmel утилиту FLIP, установить её, установить драйвер для устройства USB DFU Flip. Это очень простые операции, все достаточно подробно описано в [2].

[ПО хоста: как управлять устройством USB HID]

В пакете библиотеки LUFA [4], в папке GenericHID\HostTestApp лежат уже готовые программы для тестирования прошитой платы. Приятно было обнаружить вариант на питоне, который у меня уже был установлен (Python 2.7). Тем, у кого этого ПО нет, настоятельно советую установить [7]. Для запуска скрипта так же необходимо установить библиотеку pywinusb [8]. После того, как всё установлено, можно запустить скрипт:

python test_generic_hid_winusb.py

После запуска скрипта светодиод начал мигать – можно сказать, что мы получили «Hello world», что после нескольких дней мучений показалось просто супер чудом!

Если заглянуть в код скрипта, мы увидим, что всё достаточно просто. После запуска скрипта запускается функция main(), где сначала происходит инициализация устройства (с помощью функции get_hid_device_handle()), а затем в цикле (условием которого является подключенное устройство) раз в 0.2 секунды происходит отправка на плату команды длиной 9 байтов (1 байт зарезервирован под идентификатор репорта, а остальные получает устройство, причем 2-ой символ отвечает за то, включен или выключен наш светодиод). Отправляет данные в устройство USB функция send_led_pattern, а принимает received_led_pattern.

"""
 LUFA Library
 Copyright (C) Dean Camera, 2014.
 dean [at] fourwalledcubicle [dot] com
 www.lufa-lib.org
"""
"""
 Тестовый скрипт хоста для демонстрационного устройства USB HID (LUFA Generic HID).
Этот скрипт будет отправлять непрерывный поток стандартных репортов в устройство,
о чем будет сигнализировать изменяющееся состояние светодиодов на целевой
отладочной плате с микроконтроллером AVR USB
. Отправленные и принятые данные
будут печататься в окне терминала
. Для работы скрипт требует библиотеки pywinusb (https://pypi.python.org/pypi/pywinusb/). """ import sys from time import sleep import pywinusb.hid as hid # Стандатные идентификаторы VID, PID для устройства USB HID и длина репорта
# (полезной нагрузки). Длина репорта увеличена на 1, чтобы учесть байт для
# идентификатора репорта (
Report ID), который должен идти первым. device_vid = 0x03EB device_pid = 0x204F report_length = 1 + 8
def get_hid_device_handle(): hid_device_filter = hid.HidDeviceFilter(vendor_id=device_vid, product_id=device_pid) valid_hid_devices = hid_device_filter.get_devices() if len(valid_hid_devices) is 0: return None else: return valid_hid_devices[0] def send_led_pattern(device, led1, led2, led3, led4): # Данные репорта для демо это report ID (всегда 0), за которым # следуют данные включения/выключения светодиодов LED report_data = [0, led1, led2, led3, led4] # Остальные данные в массиве заполняются нулями report_data.extend([0] * (report_length - len(report_data))) # Отправить сгенерированные данные в устройство device.send_output_report(report_data) print("Sent LED Pattern: {0}".format(report_data[1:5])) def received_led_pattern(report_data): print("Received LED Pattern: {0}".format(report_data[1:5])) def main(): hid_device = get_hid_device_handle() if hid_device is None: print("No valid HID device found.") sys.exit(1) try: hid_device.open() print("Connected to device 0x%04X/0x%04X - %s [%s]" % (hid_device.vendor_id, hid_device.product_id, hid_device.product_name, hid_device.vendor_name)) # Настройка обработчика входных репортов HID, чтобы можно было принять
# данные от устройства:
hid_device.set_raw_data_handler(received_led_pattern) p = 0 while (hid_device.is_plugged()): # Преобразовать текущий номер светодиода p в битовую маску
# и отправка её в устройство, чтобы управлять светодиодами.
send_led_pattern(hid_device, (p >> 3) & 1, (p >> 2) & 1, (p >> 1) & 1, (p >> 0) & 1) # Вычислить номер следующего зажигаемого светодиода LED p = (p + 1) % 16 # Задержка для визуального эффекта sleep(.2) finally: hid_device.close() if __name__ == '__main__': main()

Это значит, что подцепившись к контакту P16 макетной платы AVR-USB162 (порт PD4 микроконтроллера AT90USB162), я смогу управлять реле (плюс у меня ещё и световая индикация будет - с помощью светодиода VD1, который уже установлен на плате AVR-USB162). Теперь надо было подцепить к питону кнопку (которая так же есть на плате). Кнопка подключена к порту PD7 микроконтроллера AT90USB162.

AVR-USB162-relay-and-button-sch AVR-USB162-relay-and-button-photo

[Доработка firmware USB HID под мою задачу]

Как уже писал, моя задача состоит в управлении через USB реле, и в приеме через USB сигнала от нажатия кнопки. Как управлять реле и так понятно (тест-программа HostTestApp как раз делает то что нужно), осталось разобраться с кнопкой.

Я решил не изобретать велосипед, а чуть-чуть подправить исходный код GenericHID. Я добавил в файл GenericHID.h строчку: #include < LUFA/Drivers/Board/Buttons.h >. Это нужно для того, чтобы из описания платы MICROSIN162 (это плата AVR-USB162) подцепилось описание уже имеющейся на плате кнопки.

А затем подправил код GenericHID.c таким образом, чтобы во второй байт (байт Data[1]) сообщения, которое USB HID посылает в компьютер (т. е. в ПО хоста, которое может быть либо скриптом на Python, либо программой Android), записывался 0, когда кнопка не нажата, а когда кнопка нажата, записывалась 1. Для этого внесены изменения в функцию CALLBACK_HID_Device_CreateHIDReport:

bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo,
                                         uint8_t* const ReportID,
                                         const uint8_t ReportType,
                                         void* ReportData,
                                         uint16_t* const ReportSize)
{
    uint8_t* Data        = (uint8_t*)ReportData;
    uint8_t  CurrLEDMask = LEDs_GetLEDs();
    uint8_t ButtonStatus_LCL = Buttons_GetStatus();
 
    Data[0] = (CurrLEDMask & LEDS_LED1) ? 1 : 0;
    Data[1] = (ButtonStatus_LCL & BUTTONS_BUTTON1) ? 1 : 0;
    Data[2] = (CurrLEDMask & LEDS_LED3) ? 1 : 0;
    Data[3] = (CurrLEDMask & LEDS_LED4) ? 1 : 0;
 
    *ReportSize = GENERIC_REPORT_SIZE;
    return false;
}

Как Вы уже догадались, состояние кнопки считывает вызов функции Buttons_GetStatus. Для того, чтобы порт кнопки работал как вход, я еще добавил одну строчку в функцию SetupHardware (в этой функции делаются все предварительные настройки):

Buttons_Init();

После внесения изменений проект нужно перекомпилировать (make clean, make hex), и записать в память микроконтроллера платы AVR-USB162 с помощью USB-загрузчика (как это делается, см. [2, 5]).

Теперь надо немного изменить тестовый скрипт на Python, чтобы проверить наше новое USB HID устройство.

import sys
from time import sleep
import pywinusb.hid as hid
# Идентификаторы VID, PID стандартного HID-устройства, и длина репорта/полезной нагрузки (длина
# увеличена на 1, чтобы учесть байт Report ID, который должен идти перед данными репорта)
device_vid = 0x03EB
device_pid = 0x204F
report_length = 1 + 8
 
def get_hid_device_handle():
    hid_device_filter = hid.HidDeviceFilter(vendor_id=device_vid,
                                            product_id=device_pid)
    valid_hid_devices = hid_device_filter.get_devices()
    if len(valid_hid_devices) is 0:
        return None
    else:
        return valid_hid_devices[0]
def send_led_pattern(device, led1):
    # Данные репорта для демо это report ID (всегда 0), за которым идет байт данных
    # для включения/выключения светодиода LED
    report_data = [0, led1]
    # Остальную область репорта заполняем нулями
    report_data.extend([0] * (report_length - len(report_data)))
    # Отправить сгенерированный репорт в устройство
    device.send_output_report(report_data)
    print("Sent LED Pattern: {0}".format(report_data[1:5]))

def received_led_pattern(report_data):
    print("Received LED Pattern: {0}".format(report_data[1:5]))

def main():
    hid_device = get_hid_device_handle()
    if hid_device is None:
        print("No valid HID device found.")
        sys.exit(1)
    try:
        hid_device.open()
        print("Connected to device 0x%04X/0x%04X - %s [%s]" %
              (hid_device.vendor_id, hid_device.product_id,
               hid_device.product_name, hid_device.vendor_name))
        # Установка обработчика HID input report handler для приема репортов
        hid_device.set_raw_data_handler(received_led_pattern)
        p = 0
        while (hid_device.is_plugged()):
            # Преобразование текущего индекса шаблона в битовую маску и отправка
            send_led_pattern(hid_device,
                             (p >> 0) & 1)
            # Вычислить следующий шаблон светодиода LED
            p = (p + 1) % 16
            # Небольшая задержка для визуального эффекта
            sleep(.2)
    finally:
        hid_device.close()
 
if __name__ == '__main__':
    main()

В скрипте сделаны минимальные изменения – убрана в основном цикле передача для всех светодиодов, оставлен только первый. И в функции send_led_pattern в передаваемых параметрах оставлен также один светодиод.

Теперь при запуске скрипта мы увидим, что при нажатой кнопке на месте второго байта появляется 1. Если же кнопка не нажата, мы видим 0 (принятые данные выводит обработчик received_led_pattern).

Итак, полдела сделано - получен рабочий USB-контроллер, с которого мы можем считать и отправить на него всю нужную нам информацию. Теперь надо написать программу для Андроид.

[ПО хоста для Android]

Пример работы с устройством USB HID из Android я нашел на сайте microsin.net. Внимание: чтобы программа заработала с Вашим устройством USB, обязательно нужно редактировать параметры USB под свою плату. Эти параметры, к сожалению, не лежат на «поверхности». Во многом помог сайт stackoverflow.com. А потом, я нашел информацию о кодах USB в документации LUFA.

Вот переменные класса MainActivity, без правильной установки которых ничего не заработает (весь проект приложения Android см. по ссылке [9]):

private static final int USB_TYPE_VENDOR  = 0x20; 
private static final int USB_RECIP_DEVICE = 0x01; 
private static final int USB_ENDPOINT_IN  = 0x80;
private static final int USB_ENDPOINT_OUT = 0x00;
private static final int CUSTOM_RQ_SET_STATUS = 9;
private static final int CUSTOM_RQ_GET_STATUS = 1;

Также необходимо поменять код функций getled и setled. Теперь мы передаем и принимаем строку из 8 байт. В первом байте информация о состоянии светодиода, во втором информация о нажатой кнопке.

private boolean getledstate()
{
    boolean ledstate = false;
    //Длина нашего запроса - 8 байт
    byte buf[] = new byte[8];
  
    if (usbOpenDevice())
    {
        UsbDeviceConnection connection = usbmanager.openDevice(usbdev); 
        connection.claimInterface(usbif, true);
        connection.controlTransfer(USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN,
                                   CUSTOM_RQ_GET_STATUS,
                                   0,
                                   0x0,
                                   buf,
                                   buf.length,
                                   5000);
        //Запись в лог принимаемой от контроллера информации (для отладки):
        clog("getledstate "+buf[0]+buf[1]+buf[2]+buf[3]+buf[4]+buf[5]+buf[6]+buf[7]);
        //Получение информации о текущем состоянии светодиода:
        ledstate = (buf[0]!=0)?false:true;
    }
    return ledstate;
}
 
private void setled(int value)
{
    // В коде на сайте microsin.net предлагается передавать value напрямую,
    // но у меня подобное решение не сработало. Я решил добавлять преобразованное
    // значение value в первую ячейку массива buf.
    byte buf[] = new byte[8];
    buf[0] =(byte) value;
 
    // Затем этот массив передаем в микроконтроллер.
    if (usbOpenDevice())
    {
        UsbDeviceConnection connection = usbmanager.openDevice(usbdev); 
        connection.claimInterface(usbif, true);
 
        connection.controlTransfer(USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT,
                                   CUSTOM_RQ_SET_STATUS,
                                   0,
                                   0,
                                   buf,
                                   buf.length,
                                   5000);
    }
}

В остальном код, который был взят с сайта microsin.net, остался без изменений. Программа работает, светодиод включается и выключается. При нажатии на кнопку (и опросе светодиода) мы получаем ответ о том, что во второй ячейке переменной buf появляется единичка, т. е. кнопка на плате так же работает. Если запустить цикл, который будет периодически проверять данное значение, мы сможем отслеживать нажатие кнопки.

AVR-USB162-Android-LED-on AVR-USB162-Android-LED-off

В результате мы получили заготовку устройства USB HID и тестовое приложение для Android, которые работают вместе, и передают друг другу данные через интерфейс USB.

Дело в том, что система в том виде, в котором описана в статье, будет работать только после физического подключения устройства в USB порт. Т. е. если Android выключить и включить заново, устройство не даст программе разрешения на использование устройства (не сработает intent). По идее есть возможность прописывать разрешение на доступ к конкретному устройству в самой программе, но в андроиде глюк (я прошерстил кучу форумов - это действительно глюк самой системы). Проблема в том, что система Android не запоминает в таком случае разрешение, которое дает пользователь (галка в запросе ставится, но в следующий раз спрашивает опять).

Есть способ решения - частично поправить системные файлы Android. Но данный способ непростой и достаточно ёмкий по времени. Неудобно.

И я придумал свой способ. Я запустил в программе микроконтроллера устройства USB (на плате AVR-USB162) таймер. И если за отведенное время (около 30 секунд) на плату в третий пин наша программа на андроиде не записывает единичку (читай, андроид не дал разрешения на использование устройства), плата перезагружается. Андроид определяет это как новое подключение устройства и срабатывает интент на обнаружение устройства USB.

В итоге у меня всё работает и после выключения/включения плеера Android. Может пригодится и вам когда-нибудь подобное решение.

В качестве готового исполнительного устройства заказал на ebay плату с реле [6], эту плату можно напрямую подключить к выходу микроконтроллера:

Twozilla-5V-Relay-Module

[Ссылки]

1. Tronsmart VEGA S89 site:ebay.com.
2. Макетная плата AVR-USB162.
3. LUFA - бесплатная библиотека USB для микроконтроллеров Atmel AVR.
4. LUFA site:fourwalledcubicle.com - сайт автора библиотеки LUFA. Тут можно найти ссылки на закачку последнего стабильного релиза, а также предыдущих релизов.
5. AVR4023: протокол FLIP USB DFU.
6. 1 Channel 5V Indicator Light LED Relay Module For Arduino ARM PIC New Durable site:ebay.com.
7. Python download site:python.org.
8. Pywinusb library site:pypi.python.org.
9. 141113USB-HID-Android-src.zip - исходный код устройства USB HID, проект приложения Androd (ПО хоста, управляющего устройством USB).