- El Pensadero de Canek - https://aztlan.fciencias.unam.mx/~canek/pensadero -

El control DualShock 3

(Esta entrada es la cuarta parte de una serie que cubre un proyecto personal que realicé en el verano de 2014; pueden ver todas las partes aquí [1]).

El control DualShock 3 es el control que, a partir de 2008, viene por omisión con el PlayStation 3, reemplazando (o mejor sería decirlo, extendiendo) al control SixAxis. Casi universalmente reconocido como uno de los mejores controles para videojuegos en existencia, ahora está siendo reemplazado por el DualShock 4, que es el que viene incluido con el PlayStation 4.

El DualShock 3 tiene muchas características que lo hacen interesante incluso fuera del ámbito de videojuegos; es un dispositivo Bluetooth y USB-HID casi estándar, lo que nos permite usarlo de forma casi inmediata en cualquier computadora moderna. El “casi” es porque la conectividad Bluetooth tiene un paso no estándar para autenticar al control con un PlayStation 3; y en el caso de USB-HID, existe al menos una función del dispositivo que yo no he logrado entender, pero que no es muy importante.

Lo de Bluetooth no me importa demasiado; que el dispositivo sea USB-HID en cambio lo hizo una de las piezas más en el rompecabezas de mi proyecto veraniego.

Continuando la introducción que dí de USB-HID [2], como el DualShock 3 es un dispositivo ídem, podemos analizar su reporte descriptivo en Linux utilizando lsusb. En esa entrada comentaba que para lograrlo hay que utilizar un truquito; la cosa es que si el dispositivo ya está controlado por el módulo usbhid del kernel (que ocurre de forma automática), el reporte descriptivo queda invisible, entonces hay que “liberar” al dispositivo del módulo. La explicación técnica se puede leer en este artículo de LWN.net [3], pero la manera rápida y sucia de hacerlo es haciendo

echo -n '3-1:1.0' > /sys/bus/usb/drivers/usbhid/unbind

en una terminal como superusuario; por supuesto, 3-1:1.0 es la dirección física donde conecté el DualShock 3 en mi computadora, tienen que usar la correcta en la suya.

Como sea, hecho el truco, el reporte descriptivo del DualShock 3 es este:

Report Descriptor: (length is 148)
  Item(Global): Usage Page, data= [ 0x01 ] 1
                  Generic Desktop Controls
  Item(Local ): Usage, data= [ 0x04 ] 4
                  Joystick
  Item(Main  ): Collection, data= [ 0x01 ] 1
                  Application
  Item(Main  ): Collection, data= [ 0x02 ] 2
                  Logical
  Item(Global): Report ID, data= [ 0x01 ] 1
  Item(Global): Report Size, data= [ 0x08 ] 8
  Item(Global): Report Count, data= [ 0x01 ] 1
  Item(Global): Logical Minimum, data= [ 0x00 ] 0
  Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255
  Item(Main  ): Input, data= [ 0x03 ] 3
                  Constant Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Global): Report Size, data= [ 0x01 ] 1
  Item(Global): Report Count, data= [ 0x13 ] 19
  Item(Global): Logical Minimum, data= [ 0x00 ] 0
  Item(Global): Logical Maximum, data= [ 0x01 ] 1
  Item(Global): Physical Minimum, data= [ 0x00 ] 0
  Item(Global): Physical Maximum, data= [ 0x01 ] 1
  Item(Global): Usage Page, data= [ 0x09 ] 9
                  Buttons
  Item(Local ): Usage Minimum, data= [ 0x01 ] 1
                  Button 1 (Primary)
  Item(Local ): Usage Maximum, data= [ 0x13 ] 19
                  (null)
  Item(Main  ): Input, data= [ 0x02 ] 2
                  Data Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Global): Report Size, data= [ 0x01 ] 1
  Item(Global): Report Count, data= [ 0x0d ] 13
  Item(Global): Usage Page, data= [ 0x00 0xff ] 65280
                  (null)
  Item(Main  ): Input, data= [ 0x03 ] 3
                  Constant Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Global): Logical Minimum, data= [ 0x00 ] 0
  Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255
  Item(Global): Usage Page, data= [ 0x01 ] 1
                  Generic Desktop Controls
  Item(Local ): Usage, data= [ 0x01 ] 1
                  Pointer
  Item(Main  ): Collection, data= [ 0x00 ] 0
                  Physical
  Item(Global): Report Size, data= [ 0x08 ] 8
  Item(Global): Report Count, data= [ 0x04 ] 4
  Item(Global): Physical Minimum, data= [ 0x00 ] 0
  Item(Global): Physical Maximum, data= [ 0xff 0x00 ] 255
  Item(Local ): Usage, data= [ 0x30 ] 48
                  Direction-X
  Item(Local ): Usage, data= [ 0x31 ] 49
                  Direction-Y
  Item(Local ): Usage, data= [ 0x32 ] 50
                  Direction-Z
  Item(Local ): Usage, data= [ 0x35 ] 53
                  Rotate-Z
  Item(Main  ): Input, data= [ 0x02 ] 2
                  Data Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Main  ): End Collection, data=none
  Item(Global): Usage Page, data= [ 0x01 ] 1
                  Generic Desktop Controls
  Item(Global): Report Size, data= [ 0x08 ] 8
  Item(Global): Report Count, data= [ 0x27 ] 39
  Item(Local ): Usage, data= [ 0x01 ] 1
                  Pointer
  Item(Main  ): Input, data= [ 0x02 ] 2
                  Data Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Global): Report Size, data= [ 0x08 ] 8
  Item(Global): Report Count, data= [ 0x30 ] 48
  Item(Local ): Usage, data= [ 0x01 ] 1
                  Pointer
  Item(Main  ): Output, data= [ 0x02 ] 2
                  Data Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Global): Report Size, data= [ 0x08 ] 8
  Item(Global): Report Count, data= [ 0x30 ] 48
  Item(Local ): Usage, data= [ 0x01 ] 1
                  Pointer
  Item(Main  ): Feature, data= [ 0x02 ] 2
                  Data Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Main  ): End Collection, data=none
  Item(Main  ): Collection, data= [ 0x02 ] 2
                  Logical
  Item(Global): Report ID, data= [ 0x02 ] 2
  Item(Global): Report Size, data= [ 0x08 ] 8
  Item(Global): Report Count, data= [ 0x30 ] 48
  Item(Local ): Usage, data= [ 0x01 ] 1
                  Pointer
  Item(Main  ): Feature, data= [ 0x02 ] 2
                  Data Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Main  ): End Collection, data=none
  Item(Main  ): Collection, data= [ 0x02 ] 2
                  Logical
  Item(Global): Report ID, data= [ 0xee ] 238
  Item(Global): Report Size, data= [ 0x08 ] 8
  Item(Global): Report Count, data= [ 0x30 ] 48
  Item(Local ): Usage, data= [ 0x01 ] 1
                  Pointer
  Item(Main  ): Feature, data= [ 0x02 ] 2
                  Data Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Main  ): End Collection, data=none
  Item(Main  ): Collection, data= [ 0x02 ] 2
                  Logical
  Item(Global): Report ID, data= [ 0xef ] 239
  Item(Global): Report Size, data= [ 0x08 ] 8
  Item(Global): Report Count, data= [ 0x30 ] 48
  Item(Local ): Usage, data= [ 0x01 ] 1
                  Pointer
  Item(Main  ): Feature, data= [ 0x02 ] 2
                  Data Variable Absolute No_Wrap Linear
                  Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Main  ): End Collection, data=none
  Item(Main  ): End Collection, data=none

Eso se ve medianamente intimidante; pero después de perder el tiempo con el estándar USB-HID, de hecho se vuelve legible. Voy a comentar algunas partes del reporte descriptivo, pero siguiendo su notación en bytes con comentarios en C:

0x05, 0x01,        /* Usage Page (Generic Desktop Ctrls) */
0x09, 0x04,        /*/ Usage (Joystick) */
0xA1, 0x01,        /* Collection (Physical) */

Este es sencillamente el encabezado donde especifica que lo que sigue es la descripción del un joystick.

0xA1, 0x02,        /*   Collection (Application) */
0x85, 0x01,        /*     Report ID (1) */
0x75, 0x08,        /*     Report Size (8) */
0x95, 0x01,        /*     Report Count (1) */
0x15, 0x00,        /*     Logical Minimum (0) */
0x26, 0xFF, 0x00,  /*     Logical Maximum (255) */
0x81, 0x03,        /*     Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */

El DualSock 3 reserva el primer byte; no tengo idea para qué, pero al menos en mis controles nunca envía nada distinto a 0x00.

0x75, 0x01,        /*     Report Size (1) */
0x95, 0x13,        /*     Report Count (19) */
0x15, 0x00,        /*     Logical Minimum (0) */
0x25, 0x01,        /*     Logical Maximum (1) */
0x35, 0x00,        /*     Physical Minimum (0) */
0x45, 0x01,        /*     Physical Maximum (1) */
0x05, 0x09,        /*     Usage Page (Button) */
0x19, 0x01,        /*     Usage Minimum (0x01) */
0x29, 0x13,        /*     Usage Maximum (0x13) */
0x81, 0x02,        /*     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */
0x75, 0x01,        /*     Report Size (1) */
0x95, 0x0D,        /*     Report Count (13) */
0x06, 0x00, 0xFF,  /*     Usage Page (Vendor Defined 0xFF00) */
0x81, 0x03,        /*     Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */

4 bytes, o un entero de 32 bits (es exactamente lo mismo), para representar potencialmente 32 botones; actualmente sólo 19 de hecho son considerados (bits 0-18), y 13 reservados para uso futuro (bits 19-31).

0x15, 0x00,        /*     Logical Minimum (0) */
0x26, 0xFF, 0x00,  /*     Logical Maximum (255) */
0x05, 0x01,        /*     Usage Page (Generic Desktop Ctrls) */
0x09, 0x01,        /*     Usage (Pointer) */
0xA1, 0x00,        /*     Collection (Undefined) */
0x75, 0x08,        /*       Report Size (8) */
0x95, 0x04,        /*       Report Count (4) */
0x35, 0x00,        /*       Physical Minimum (0) */
0x46, 0xFF, 0x00,  /*       Physical Maximum (255) */
0x09, 0x30,        /*       Usage (X) */
0x09, 0x31,        /*       Usage (Y) */
0x09, 0x32,        /*       Usage (Z) */
0x09, 0x35,        /*       Usage (Rz) */
0x81, 0x02,        /*       Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */
0xC0,              /*     End Collection */

Cuatro ejes: X, Y, Z, y rotación de Z. Cada eje es un byte (“Report Size (8)”), y se toma el valor del byte menos 127 como el valor del eje; en otras palabras, 0 – 127 = -127 es el eje en su posición más baja (o a la izquierda), 127 – 127 = 0 es el eje en “reposo” (centrado), y 255 – 127 = 128 es el eje en su posición más alta (o a la derecha). El DualShock 3 utiliza el estándar HID para definir eje Z y rotación de Z, pero esos dos realmente son los ejes del joystick derecho, y los dos primeros ejes son los del joystick izquierdo.

0x05, 0x01,        /*     Usage Page (Generic Desktop Ctrls) */
0x75, 0x08,        /*     Report Size (8) */
0x95, 0x27,        /*     Report Count (39) */
0x09, 0x01,        /*     Usage (Pointer) */
0x81, 0x02,        /*     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */
0x75, 0x08,        /*     Report Size (8) */
0x95, 0x30,        /*     Report Count (48) */
0x09, 0x01,        /*     Usage (Pointer) */
0x91, 0x02,        /*     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */
0x75, 0x08,        /*     Report Size (8) */
0x95, 0x30,        /*     Report Count (48) */
0x09, 0x01,        /*     Usage (Pointer) */
0xB1, 0x02,        /*     Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */
0xC0,              /*   End Collection */
0xA1, 0x02,        /*   Collection (Application) */
0x85, 0x02,        /*     Report ID (2) */
0x75, 0x08,        /*     Report Size (8) */
0x95, 0x30,        /*     Report Count (48) */
0x09, 0x01,        /*     Usage (Pointer) */
0xB1, 0x02,        /*     Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */
0xC0,              /*   End Collection */
0xA1, 0x02,        /*   Collection (Application) */
0x85, 0xEE,        /*     Report ID (238) */
0x75, 0x08,        /*     Report Size (8) */
0x95, 0x30,        /*     Report Count (48) */
0x09, 0x01,        /*     Usage (Pointer) */
0xB1, 0x02,        /*     Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */
0xC0,              /*   End Collection */
0xA1, 0x02,        /*   Collection (Application) */
0x85, 0xEF,        /*     Report ID (239) */
0x75, 0x08,        /*     Report Size (8) */
0x95, 0x30,        /*     Report Count (48) */
0x09, 0x01,        /*     Usage (Pointer) */
0xB1, 0x02,        /*     Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */
0xC0,              /*   End Collection */
0xC0,              /* End Collection */

Esta última parte explica cómo el reporte entero del dispositivo utiliza 48 bytes (más uno de identificador): 3 colecciones de 8 bytes de “controles de escritorio genérico” (“Generic Desktop Ctrls”), y tres colecciones de 8 bytes de aplicación.

El byte 0 sirve de identificador y (hasta donde he visto) es siempre 1.

El byte 1 (como dije arriba) está reservado; los siguientes 4 bytes (bytes 2-5) tienen información de los botones: bit prendido siginifica que el botón está presionado; apagado implica no presionado. Aunque son 32 bits en total (4 bytes), realmente sólo se usan 19, con 13 reservados.

Los bytes 6, 7, 8 y 9 son los ejes de los joysticks, y a partir del 10 (realmente 13; 10, 11 y 12 sólo me reportan ceros) hasta el 24 son las presiones de cada botón: 0 es el botón casi no está presionado, 255 es el botón está completamente presionado. Esas son las 3 colecciones de escritorio genérico (24 bytes). Los otros 24 bytes contienen las colecciones de aplicación, pero hasta donde entiendo sólo se usan los bytes 41-48; 2 bytes para el acelerómetro X, 2 bytes para el acelerómetro Y, 2 bytes para el acelerómetro Z, y dos bytes para el giroscopio. No tengo idea qué carajo hagan los bytes 25-40, pero en uno de mis controles siempre reporta:

00 00 00 00 03 ef 16 00 00 00 00 33 ae 77 00 40

A lo mejor ahí estarán las presiones de los 13 botones reservados. Por cierto, todo esto es como yo lo he entendido; si estoy entendiendo algo mal, por favor díganme.

¿Para qué sirve todo esto? Para mi proyecto de verano, no me sirvió de mucho; pero sí será de utilidad (de hecho ya lo fue) para otro miniproyecto relacionado. Con la información de arriba puedo leer bit a bit la información que un DualShock 3 envía; pero esto, además de que hay formas mucho más sencillas de hacerlo (por no decir de mucho más alto nivel), cualquier güey lo hace. Yo quería hacer algo más interesante (y lo hice).

En la siguiente parte de esta serie explicaré la pieza principal del rompecabezas. No sé si con ello ya quede claro mi proyecto; pero si no, la entrada que le seguirá debería dejarlo claro.