Dispositivo Gadget HID Usando el ODROID-C2

Feature Image for the HID gadget device on ODROID-C2 article

La siguiente guía describe cómo configurar el ODROID-C2 as a HID gadget device, en este caso lo utilizaremos como un teclado o gamepad básico.

Los siguientes pasos pueden adaptarse para cualquier otro dispositivo que

  • Soporte el modo USB OTG
  • Tenga un kernel Linux superior a la versión 3.19 con el módulo FunctionFS HID compilado, nosotros utilizaremos Linux versión 4.15.

Antes de empezar, debes tener en cuenta que todos los comandos deben ejecutarse como root.

HID gadget device with The ODROID-C2 using USB OTG device mode
Figura 1 - El ODROID-C2 puede actuar como dispositivo HID usando el modo USB OTG

Preparación

La forma más fácil para ejecutar un kernel reciente, por ahora, es usar la imagen ArchLinuxARM de https://archlinuxarm.org/platforms/armv8/amlogic/odroid-c2. Después de seguir las instrucciones y haber finalizado la instalación por defecto, actualiza el sistema para usar el kernel principal (4.15 en el momento de escribir esta guía):

$ sudo pacman -Syu
$ sudo pacman -R uboot-odroid-c2
$ sudo pacman -S uboot-odroid-c2-mainline linux-aarch64 dtc
Asegúrate de NO reiniciar el dispositivo todavía, si estás arrancando desde la eMMC. La instalación por defecto tiene algunas peculiaridades que hacen que el sistema sea de sólo lectura y esté deshabilitado el módulo OTG. Primero debemos desmontar el Device Tree Blob o DTB, archivo utilizado por el C2 para activar los periféricos integrados en su código fuente desde el DTS
$ cd /boot/dtbs/amlogic/
$ sudo cp -p meson-gxbb-odroidc2{,_backup}.dtb
$ sudo dtc -I dtb -O dts meson-gxbb-odroidc2.dtb > meson-gxbb-odroidc2.dts
Una vez hecho esto, edita el código fuente:
$ sudo nano meson-gxbb-odroidc2.dts
En la sección [mmc@74000], cambia la siguiente línea para volver a activar el acceso de escritura de la eMMC (incluso si no estás usando una eMMC en estos momentos, siempre es bueno cambiarlo):
max-frequency = <0xbebc200>;
pr:
max-frequency = <0x8f0d180>;
Y en la sección [usb@c9000000], cambia:
dr_mode = "host";
por:
dr_mode = "peripheral";
usb@c9000000 es el periférico OTG que está en modo "host" por defecto. Ten cuidado de no tocar nada relativo al usb@c910000 que es el Hub USB de 4 puertos. Una vez hecho todo esto, podemos volver a compilar el archivo DTB y reiniciar:
$ sudo dtc -I dts -O dtb meson-gxbb-odroidc2.dts > meson-gxbb-odroidc2.dtb
$ sudo reboot
Podemos comprobar qué kernel se está ejecutando con el siguiente comando:
$ uname -r
Debería ser 4.15+ y si todo funcionaba bien, el siguiente comando debería mostrar un enlace simbólico [c9000000.usb]:
$ ls /sys/class/udc/

Configuración

Utiliza el siguiente script python3 que realiza la configuración y desmonta el dispositivo de forma automática:

import sys
import os
import shutil
import pwd
import asyncio
import subprocess
import argparse
import atexit

class HIDReportDescriptorKeyboard(object):
def __len__(self):
return 8

def __bytes__(self):
return bytes([
 0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
 0x09, 0x06, # Usage (Keyboard)
 0xA1, 0x01, # Collection (Application)
 0x05, 0x07, # Usage Page (Kbrd/Keypad)
 0x19, 0xE0, # Usage Minimum (0xE0)
 0x29, 0xE7, # Usage Maximum (0xE7)
 0x15, 0x00, # Logical Minimum (0)
 0x25, 0x01, # Logical Maximum (1)
 0x75, 0x01, # Report Size (1)
 0x95, 0x08, # Report Count (8)
 0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
 0x95, 0x01, # Report Count (1)
 0x75, 0x08, # Report Size (8)
 0x81, 0x03, # Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
 0x95, 0x05, # Report Count (5)
 0x75, 0x01, # Report Size (1)
 0x05, 0x08, # Usage Page (LEDs)
 0x19, 0x01, # Usage Minimum (Num Lock)
 0x29, 0x05, # Usage Maximum (Kana)
 0x91, 0x02, # Output (Data,Var,Abs)
 0x95, 0x01, # Report Count (1)
 0x75, 0x03, # Report Size (3)
 0x91, 0x03, # Output (Const,Var,Abs)
 0x95, 0x06, # Report Count (6)
 0x75, 0x08, # Report Size (8)
 0x15, 0x00, # Logical Minimum (0)
 0x25, 0x65, # Logical Maximum (101)
 0x05, 0x07, # Usage Page (Kbrd/Keypad)
 0x19, 0x00, # Usage Minimum (0x00)
 0x29, 0x65, # Usage Maximum (0x65)
 0x81, 0x00, # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
 0xC0, # End Collection
])

class HIDReportDescriptorGamepad(object):
def __len__(self):
return 4

def __bytes__(self):
return bytes([
 0x05, 0x01, # USAGE_PAGE (Generic Desktop)
 0x15, 0x00, # LOGICAL_MINIMUM (0)
 0x09, 0x04, # USAGE (Joystick)
 0xa1, 0x01, # COLLECTION (Application)
 0x05, 0x02, # USAGE_PAGE (Simulation Controls)
 0x09, 0xbb, # USAGE (Throttle)
 0x15, 0x81, # LOGICAL_MINIMUM (-127)
 0x25, 0x7f, # LOGICAL_MAXIMUM (127)
 0x75, 0x08, # REPORT_SIZE (8)
 0x95, 0x01, # REPORT_COUNT (1)
 0x81, 0x02, # INPUT (Data,Var,Abs)
 0x05, 0x01, # USAGE_PAGE (Generic Desktop)
 0x09, 0x01, # USAGE (Pointer)
 0xa1, 0x00, # COLLECTION (Physical)
 0x09, 0x30, # USAGE (X)
 0x09, 0x31, # USAGE (Y)
 0x95, 0x02, # REPORT_COUNT (2)
 0x81, 0x02, # INPUT (Data,Var,Abs)
 0xc0, # END_COLLECTION
 0x09, 0x39, # USAGE (Hat switch)
 0x15, 0x00, # LOGICAL_MINIMUM (0)
 0x25, 0x03, # LOGICAL_MAXIMUM (3)
 0x35, 0x00, # PHYSICAL_MINIMUM (0)
 0x46, 0x0e, 0x01, # PHYSICAL_MAXIMUM (270)
 0x65, 0x14, # UNIT (Eng Rot:Angular Pos)
 0x75, 0x04, # REPORT_SIZE (4)
 0x95, 0x01, # REPORT_COUNT (1)
 0x81, 0x02, # INPUT (Data,Var,Abs)
 0x05, 0x09, # USAGE_PAGE (Button)
 0x19, 0x01, # USAGE_MINIMUM (Button 1)
 0x29, 0x04, # USAGE_MAXIMUM (Button 4)
 0x15, 0x00, # LOGICAL_MINIMUM (0)
 0x25, 0x01, # LOGICAL_MAXIMUM (1)
 0x75, 0x01, # REPORT_SIZE (1)
 0x95, 0x04, # REPORT_COUNT (4)
 0x55, 0x00, # UNIT_EXPONENT (0)
 0x65, 0x00, # UNIT (None)
 0x81, 0x02, # INPUT (Data,Var,Abs)
 0xc0 # END_COLLECTION
])

class HidDaemon(object):
 def __init__(self, vendor_id, product_id, manufacturer, description, serial_number, hid_report_class):
 self._descriptor = hid_report_class()
 self._hid_devname = 'odroidc2_hid'
 self._vendor = vendor_id
 self._product = product_id
 self._manufacturer = manufacturer
 self._desc = description
 self._serial = serial_number
 self._libcomposite_already_running = self.check_libcomposite()
 self._usb_f_hid_already_running = self.check_usb_f_hid()
 self._loop = asyncio.get_event_loop()
 self._devname = 'hidg0'
 self._devpath = '/dev/%s' % self._devname

def _cleanup(self):
 udc_path = '/sys/kernel/config/usb_gadget/%s/UDC' % self._hid_devname
 if os.path.exists(udc_path):
 with open(udc_path, 'w') as fd:
 fd.truncate()
 try:
 shutil.rmtree('/sys/kernel/config/usb_gadget/%s' % self._hid_devname, ignore_errors=True)
 except:
 pass
 if not self._usb_f_hid_already_running and self.check_usb_f_hid():
 self.unload_usb_f_hid()
 if not self._libcomposite_already_running and self.check_libcomposite():
 self.unload_libcomposite()

@staticmethod
def check_libcomposite():
 r = int(subprocess.check_output("lsmod | grep 'libcomposite' | wc -l", shell=True, close_fds=True).decode().strip())
 return r != 0

@staticmethod
def load_libcomposite():
 if not HidDaemon.check_libcomposite():
 subprocess.check_call("modprobe libcomposite", shell=True, close_fds=True)

@staticmethod
def unload_libcomposite():
 if HidDaemon.check_libcomposite():
 subprocess.check_call("rmmod libcomposite", shell=True, close_fds=True)

@staticmethod
def check_usb_f_hid():
 r = int(
 subprocess.check_output("lsmod | grep 'usb_f_hid' | wc -l", shell=True, close_fds=True).decode().strip())
 return r != 0

@staticmethod
def load_usb_f_hid():
 if not HidDaemon.check_libcomposite():
 subprocess.check_call("modprobe usb_f_hid", shell=True, close_fds=True)

@staticmethod
def unload_usb_f_hid():
 if HidDaemon.check_libcomposite():
 subprocess.check_call("rmmod usb_f_hid", shell=True, close_fds=True)

def _setup(self):
f_dev_name = self._hid_devname
os.makedirs('/sys/kernel/config/usb_gadget/%s/strings/0x409' % f_dev_name, exist_ok=True)
os.makedirs('/sys/kernel/config/usb_gadget/%s/configs/c.1/strings/0x409' % f_dev_name, exist_ok=True)
os.makedirs('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0' % f_dev_name, exist_ok=True)
with open('/sys/kernel/config/usb_gadget/%s/idVendor' % f_dev_name, 'w') as fd:
fd.write('0x%04x' % self._vendor)
with open('/sys/kernel/config/usb_gadget/%s/idProduct' % f_dev_name, 'w') as fd:
fd.write('0x%04x' % self._product)
with open('/sys/kernel/config/usb_gadget/%s/bcdDevice' % f_dev_name, 'w') as fd:
fd.write('0x0100')
with open('/sys/kernel/config/usb_gadget/%s/bcdUSB' % f_dev_name, 'w') as fd:
fd.write('0x0200')

with open('/sys/kernel/config/usb_gadget/%s/strings/0x409/serialnumber' % f_dev_name, 'w') as fd:
fd.write(self._serial)
with open('/sys/kernel/config/usb_gadget/%s/strings/0x409/manufacturer' % f_dev_name, 'w') as fd:
fd.write(self._manufacturer)
with open('/sys/kernel/config/usb_gadget/%s/strings/0x409/product' % f_dev_name, 'w') as fd:
fd.write(self._desc)

with open('/sys/kernel/config/usb_gadget/%s/configs/c.1/strings/0x409/configuration' % f_dev_name, 'w') as fd:
fd.write('Config 1 : %s' % self._desc)
with open('/sys/kernel/config/usb_gadget/%s/configs/c.1/MaxPower' % f_dev_name,'w') as fd:
fd.write('250')

with open('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0/protocol' % f_dev_name, 'w') as fd:
fd.write('1')
with open('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0/subclass' % f_dev_name, 'w') as fd:
fd.write('1')
with open('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0/report_length' % f_dev_name, 'w') as fd:
fd.write(str(len(self._descriptor)))
with open('/sys/kernel/config/usb_gadget/%s/functions/hid.usb0/report_desc' % f_dev_name, 'wb') as fd:
fd.write(bytes(self._descriptor))

os.symlink(
'/sys/kernel/config/usb_gadget/%s/functions/hid.usb0' % f_dev_name,
'/sys/kernel/config/usb_gadget/%s/configs/c.1/hid.usb0' % f_dev_name,
target_is_directory=True
)

with open('/sys/kernel/config/usb_gadget/%s/UDC' % f_dev_name, 'w') as fd: fd.write('\r\n'.join(os.listdir('/sys/class/udc')))

def run(self):
if not self._libcomposite_already_running:
self.load_libcomposite()
atexit.register(self._cleanup)

# Setup HID gadget (keyboard)
self._setup()

# Use asyncio because we can then do thing on the side (web ui, polling attached devices using pyusb ...)
try:
self._loop.run_forever()
except KeyboardInterrupt:
pass

if __name__ == '__main__':
user_root = pwd.getpwuid(0)
user_curr = pwd.getpwuid(os.getuid())
print('Running as <%s>' % user_curr.pw_name)
if os.getuid() != 0:
print('Attempting to run as ')
sys.exit(os.system("/usr/bin/sudo /usr/bin/su root -c '%s %s'" % (sys.executable, ' '.join(sys.argv))))
parser = argparse.ArgumentParser()
parser.add_argument('hid_type', choices=['keyboard', 'gamepad'])
args = parser.parse_args()
if args.hid_type == 'keyboard':
print('Emulating: Keyboard')
# Generic keyboard
hid = HidDaemon(0x16c0, 0x0488, 'author', 'ODROID C2 KBD HID', 'fedcba9876543210', HIDReportDescriptorKeyboard)
hid.run()
elif args.hid_type == 'gamepad':
print('Emulating: Gamepad')
# Teensy FlightSim for the purpose of this example (and since it's intended for DIY, it fits ou purpose)
hid = HidDaemon(0x16c0, 0x0488, 'author', 'ODROID C2 GAMEPAD HID', 'fedcba9876543210', HIDReportDescriptorGamepad)
hid.run()
Las clases HIDReportDescriptorKeyboard y HIDReportDescriptorGamepad son donde describimos nuestro dispositivo, su tipo, los botones y la cantidad de ejes. Ten en cuenta que vendorId y productId también son muy importantes puesto que, aunque describas tu dispositivo como un gamepad, si VID/PID son los de un teclado, el sistema operativo probablemente identifique el dispositivo como un teclado.

A continuación, ejecuta el siguiente comando, que requiere privilegios de root, con el fin de crear un dispositivo /dev/hidg0 con el que puedas escribir libremente:

$ sudo python3 script.py keyboard
o
$ sudo python3 script.py gamepad
Después podemos probarlo con el argumento del teclado:
$ sudo sleep 5 && echo -ne "\0\0\x4\0\0\0\0\0" > /dev/hidg0 && echo -ne "\0\0\0\0\0\0\0\0" > /dev/hidg0
Este comando escribirá "A" (o "Q" si usas una distribución azerty), tras 5 segundos. Ahora, para probarlo con el argumento del gamepad usa:
$ sudo sleep 5 && echo -ne "\0\0\0\x8c" > /dev/hidg0 && echo -ne "\0\0\0\0" > /dev/hidg0
Esto activará el cuarto botón del dispositivo gamepad.

Creando diferentes dispositivos

Los dos ejemplos usan descriptores muy básicos que usan el siguiente formato de bit al escribir en /dev/hidg0:

Teclado (6 rotaciones)

BYTE 1 BYTE 2 BYTE 3 BYTE 4 BYTE 5 BYTE 6 BYTE 7 BYTE 8
Modificadores Reservado Tecla 1 Tecla 2 Tecla 3 Tecla 4 Tecla 5 Tecla 6

Gamepad

BYTE 1 BYTE 2
1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
Acelerador (-127 to 127) Eje-X (-127 to 127)
BYTE 4
1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
Eje-Y (-127 to 127) B4 B3 B2 B1 (1) (2)

(1)Cuando es igual a 0b11, los botones HAT están configurados como no activos, que debería ser el valor por defecto. (2)Mascara de bits botones HAT, dado que los bits 5-6 están configurados en 0 (a)0b00 => ARRIBA (b)0b01 => DERECHA (c)0b10 => ABAJO (d)0b11 => IZQUIERDA

Conclusión

Este es un ejemplo muy básico, aunque nos muestra las opciones que tenemos disponibles. El análisis de las especificaciones USB y HID nos da una idea de los muchos posibles usos:

  • Simular un dispositivo, como un teclado, gamepad, ratón específicos para fines de desarrollo.
  • Hacer que un dispositivo aparezca como otro. Para dispositivos heredados
  • Pura depuración/ingeniería inversa USB (proyecto USBProxy en https://github.com/dominicgs/USBProxy).
  • Pruebas de penetración.
  • Y seguramente muchos otros en los que no he pensado.

Para comentarios, preguntas y sugerencias, visita el hilo original en https://forum.odroid.com/viewtopic.php?f=139&t=30267.

Recursos

Especificaciones del USB HID 1.1 http://www.usb.org/developers/hidpage/HID1_11.pdf

Dónde encontrar los códigos de las teclas para el teclado https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2

Para entender mejor los descriptores de los informes https://hamaluik.com/posts/making-a-custom-teensy3-hid-joystick/ y http://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/

Herramienta para hacer descripciones (bastante tosca pero oficial) http://www.usb.org/developers/hidpage#HID%20Descriptor%20Tool

Descriptor de informe de teclado http://isticktoit.net/?p=1383

Utilidad para verificar los descriptores de informes http://eleccelerator.com/usbdescreqparser/

Modificación DTB de periférico OTG https://community.nxp.com/thread/383191

Soporte para el kernel principal del C2 (ajustes de frecuencia) https://forum.odroid.com/viewtopic.php?f=135&t=22717

Be the first to comment

Leave a Reply