paint-brush
Ingeniería inversa de luces LED con Python para convertir el monitor de su computadora en un monitor ambientalpor@reaminated
2,401 lecturas
2,401 lecturas

Ingeniería inversa de luces LED con Python para convertir el monitor de su computadora en un monitor ambiental

por Reaminated12m2023/03/31
Read on Terminal Reader

Demasiado Largo; Para Leer

Convierta su monitor normal en un monitor ambiental con luces que cambian para adaptarse a los colores que aparecen actualmente en la pantalla. Usando Python y luces RGB, este es un tutorial para mostrar cómo mejorar su experiencia cinematográfica y de juego.
featured image - Ingeniería inversa de luces LED con Python para convertir el monitor de su computadora en un monitor ambiental
Reaminated HackerNoon profile picture
0-item

Introducción

Para aquellos que no están familiarizados con los televisores ambientales, es una forma de suavizar el salto desde el borde de la pantalla del televisor y su entorno inmediato para brindar una experiencia más inmersiva. Tenía algunas luces LED por ahí y decidí ver si era posible controlar las luces a través de un código y, a su vez, convertir la pantalla de mi computadora en un monitor ambiental. Si bien quería usarlo para mi monitor, se puede usar en cualquier lugar y con cualquier color que pueda enviar, incluidas otras funciones que pueden tener sus luces, como reacciones de audio o patrones aleatorios. He tenido la intención de escribir esta publicación durante un tiempo ya que la he estado usando en un monitor anterior, pero nunca pude agregarla a mi nuevo monitor, así que la documenté a medida que avanzaba para cualquiera que pudiera encontrar es útil ¡Vamos a por ello! (Tenga en cuenta que es probable que las luces LED sean Bluetooth Low Energy (BLE), por lo que su computadora deberá ser compatible con BLE para poder interactuar con ellas). El código completo está en GitHub .

Pasos de alto nivel

  • Descubra qué comandos acepta el receptor Bluetooth de la luz LED
  • Enviar comandos a las luces LED a través del Bluetooth de mi computadora
  • Obtener el color dominante de la pantalla actual
  • Envía el color dominante a las luces LED

requisitos previos

  • Luces LED RGB compatibles con Bluetooth y la aplicación que lo acompaña (estoy usando Android, es probable que iOS requiera un enfoque alternativo al que se describe aquí, pero debería ser posible usar Wireshark directamente para monitorear el tráfico de Bluetooth). Adjunté estas luces a la parte posterior de mi monitor.
  • Tiburón alambre
  • Herramientas SDK de Android (específicamente adb.exe)
  • Herramientas de desarrollador (usaré Python 3.10, aunque cualquier versión 3.x debería funcionar, pero los principios deberían ser los mismos para el idioma que prefiera)
  • Un dispositivo desde el que enviar comandos BLE (p. ej., un portátil compatible con BLE)


Obtener datos de Bluetooth

El primer paso que debemos hacer es asegurarnos de que la aplicación que viene con las luces funcione como se espera. Esto se puede probar fácilmente ejecutando la aplicación original de la luz y asegurándose de que las luces reaccionen en consecuencia según los botones de encendido/apagado/iluminación que esté presionando en su aplicación. Hacemos esto porque en breve estaremos presionando y detectando los códigos específicos enviados al receptor Bluetooth en las luces.


Hay dos enfoques que podría tomar. Una era descompilar el archivo JAR de la aplicación y encontrar los códigos que se enviaban, pero quería aprender más sobre el protocolo Bluetooth, así que opté por registrar toda la actividad de Bluetooth en mi Android y extraerla de allí. Así es cómo:


  1. Habilite las opciones de desarrollador en su dispositivo Android.


  2. Habilite el registro de indagación de Bluetooth HCI (HCI significa Interfaz de controlador de host). Puede encontrar esta opción en Configuración> Sistema> Desarrollador o buscarla en la configuración como en la imagen a continuación.


Habilitación de registros de HCI Snoop


  1. Ahora necesitamos realizar acciones específicas para que podamos identificar qué envía cada acción al receptor Bluetooth de la luz. Voy a mantenerlo simple en Encendido/Rojo/Verde/Azul/Apagado, en ese orden, pero si sus luces son compatibles con otras funciones, también puede jugar con ellas.


  2. Ejecute la aplicación y presione Encendido, Rojo, Verde, Azul y Apagado. También puede ser útil vigilar el tiempo aproximado para que sea más fácil filtrar si tienes mucha actividad de Bluetooth en tu dispositivo.


Grabación de comandos para enviar


  1. Apague el Bluetooth para que no tengamos más ruido. En los siguientes pasos, analizaremos los comandos de Bluetooth y, como sabemos el orden de lo que presionamos, podemos averiguar qué valores corresponden a qué botón presionamos.


  2. Ahora necesitamos acceder a los registros de Bluetooth en el teléfono. Hay varias formas de hacer esto, pero generaré y exportaré un informe de error. Para hacer esto, habilite la depuración de USB en la configuración del teléfono, conecte el teléfono a la computadora y use la herramienta de línea de comandos adb.exe.


     adb bugreport led_bluetooth_report


  3. Esto generará un archivo zip en el directorio local de su computadora con el nombre de archivo " led_bluetooth_report.zip ". Puede especificar una ruta si lo prefiere (por ejemplo, C:\MyPath\led_bluetooth_report")


  4. Dentro de este zip están los registros que necesitamos. Esto puede variar de un dispositivo a otro (comente si lo encontró en otro lugar de su dispositivo). En mi teléfono Google Pixel, estaba en FS\data\misc\bluetooth\logs\btsnoop_hci.log


  5. Ahora que tenemos los archivos de registro, ¡vamos a analizarlos! Para hacer esto, decidí usar Wireshark, así que inicie Wireshark y vaya a Archivo... Abrir... y seleccione el archivo de registro btsnoop_hci.


Si bien puede parecer desalentador, hagamos que sea más fácil para nosotros encontrar lo que estamos buscando al filtrar el BTL2CAP en 0x0004, que es el Protocolo de atributos en el código fuente de Wireshark . El protocolo de atributo define la forma en que dos dispositivos BLE se comunican entre sí, por lo que esto es lo que necesitamos para ayudar a encontrar cómo se comunica la aplicación con las luces. Puede filtrar los registros en Wireshark escribiendo btl2cap.cid == 0x0004 en la barra " Aplicar un filtro de visualización " cerca de la parte superior y presione Entrar


Filtra los registros para hacerlo más fácil


Ahora hemos filtrado el registro; debería facilitar la búsqueda de los comandos. Podemos ver las marcas de tiempo (Ir a Ver... Formato de visualización de la hora... Hora del día para convertir la hora si tiene un formato incorrecto). Queremos ver los registros de comandos de escritura enviados, ya que son aquellos a los que enviamos un valor a las luces. Suponiendo que su tiempo más reciente se encuentra en la parte inferior, desplácese hacia abajo hasta los últimos cinco eventos. Estos deben ser Encendido, Rojo, Verde, Azul y Apagado en ese orden, siendo Apagado el último.


BS_ADDR y Valores - ¡La información que necesitamos!


Tome nota del destino BD_ADDR, ya que lo necesitaremos en breve, y póngase su sombrero de Sherlock Holmes, ya que aquí es donde debemos desbloquear el patrón de cómo se codifican los colores y los comandos de encendido/apagado dentro del mensaje. Esto variará según el fabricante de la luz, pero aquí está la lista de valores que obtuve para mi dispositivo:


  • En: 7e0404f00001ff00ef
  • Rojo: 7e070503ff000010ef
  • Verde: 7e07050300ff0010ef
  • Azul: 7e0705030000ff10ef
  • Desactivado: 7e0404000000ff00ef


Estos son claramente valores hexadecimales y, si observa detenidamente, verá que hay algunos patrones fijos. Dividamos los patrones ya que esto debería hacer las cosas mucho más claras.


  • Activado: 7e0404 f00001 ff00ef
  • Rojo: 7e070503 ff0000 10ef
  • Verde: 7e070503 00ff00 10ef
  • Azul: 7e070503 0000ff 10ef
  • Apagado: 7e0404 000000 ff00ef


Para aquellos familiarizados con los valores hexadecimales de rojo, verde y azul puros, sabrán que los valores son #FF000, #00FF00 y #0000FF, respectivamente, que es exactamente lo que podemos ver arriba. ¡Esto significa que ahora conocemos el formato para cambiar los colores a lo que queramos! (o al menos de lo que son capaces las propias luces). También podemos ver que On y Off tienen un formato diferente de los colores y son similares entre sí, On tiene f00001 y Off tiene 00000.

¡Eso es todo! Ahora tenemos suficiente información para comenzar a codificar e interactuar con las luces.

Conexión a luces LED

Hay tres cosas clave que necesitamos:


  • La dirección del dispositivo (este es el Destino BD_ADDR de arriba)

  • Los valores a enviar al dispositivo (los valores hexadecimales obtenidos anteriormente)

  • La característica que queremos cambiar. Una característica de Bluetooth LE es una estructura de datos que esencialmente define los datos que se pueden enviar entre un host y los dispositivos Bluetooth del cliente. Necesitamos encontrar la característica (un UUID de 16 o 128 bits) que hace referencia a las luces. Hay algunos números asignados de uso común que se pueden encontrar aquí, pero a menos que el dispositivo se ajuste a ellos, podrían estar usando un UUID personalizado. Como mis luces no están en la lista de números asignados, busquémosla mediante un código.


Estoy usando Python 3.10 y Bleak 0.20.1 . Asegúrese de que Bluetooth en su computadora esté activado (no es necesario emparejarlo con el dispositivo, nos conectaremos a él a través del código).


 # Function to create a BleakClient and connect it to the address of the light's Bluetooth reciever async def init_client(address: str) -> BleakClient: client = BleakClient(address) print("Connecting") await client.connect() print(f"Connected to {address}") return client # Function we can call to make sure we disconnect properly otherwise there could be caching and other issues if you disconnect and reconnect quickly async def disconnect_client(client: Optional[BleakClient] = None) -> None: if client is not None : print("Disconnecting") if characteristic_uuid is not None: print(f"charUUID: {characteristic_uuid}") await toggle_off(client, characteristic_uuid) await client.disconnect() print("Client Disconnected") print("Exited") # Get the characteristic UUID of the lights. You don't need to run this every time async def get_characteristics(client: BleakClient) -> None: # Get all the services the device (lights in this case) services = await client.get_services() # Iterate the services. Each service will have characteristics for service in services: # Iterate and subsequently print the characteristic UUID for characteristic in service.characteristics: print(f"Characteristic: {characteristic.uuid}") print("Please test these characteristics to identify the correct one") await disconnect_client(client)


He comentado el código, por lo que debería explicarse por sí mismo, pero esencialmente, nos conectamos a las luces y encontramos todas las características que expone. Mi salida fue:


Característica: 00002a00-0000-1000-8000-00805F9B34FB Característica: 00002A01-0000-1000-8000-00805F9B34FB Característica: 0000FFF3-0000-1000-8000-00805F9B34FB Característica: 0000ff4-0000-1000-8000-0080805805805555444444444444444b34m


Un rápido Google de los dos primeros UUID muestra que esto se refiere al nombre y la apariencia del servicio, lo cual es irrelevante para nosotros. Sin embargo, el tercero y el cuarto parecen los más adecuados, siendo el tercero ( 0000fff3-0000-1000-8000-00805f9b34fb ) la característica de escritura según esta página . Excelente, ahora tenemos la característica que necesitamos para que este dispositivo en particular escriba con un valor (el color hexadecimal).

Control de luces LED

Finalmente tenemos todas las piezas que necesitamos. En esta etapa, puede ser creativo con la entrada de color que le gustaría usar. Podría, por ejemplo, conectar las luces a una API del mercado comercial para cambiar los colores de acuerdo con el rendimiento de su cartera. En este caso, queremos que nuestros monitores sean conscientes del ambiente, por lo que debemos obtener el color dominante de la pantalla y enviarlo.


Hay muchas maneras de hacer esto, así que siéntase libre de experimentar con cualquier algoritmo que desee. Uno de los enfoques más simples sería iterar cada X número de píxeles en la pantalla y tomar un promedio, mientras que las soluciones más complicadas buscarían colores que los ojos humanos perciban como más dominantes. ¡Siéntase libre de comentar cualquier hallazgo que le gustaría compartir!

Por el bien de esta publicación de blog, lo simplificaré utilizando el método get_dominant_color de la biblioteca fast_colorthief .


 ''' Instead of taking the whole screensize into account, I'm going to take a 640x480 resolution from the middle. This should make it faster but you can toy around depending on what works for you. You may, for example, want to take the outer edge colours instead so it the ambience blends to the outer edges and not the main screen colour ''' screen_width, screen_height = ImageGrab.grab().size #get the overall resolution size region_width = 640 region_height = 480 region_left = (screen_width - region_width) // 2 region_top = (screen_height - region_height) // 2 screen_region = (region_left, region_top, region_left + region_width, region_top + region_height) screenshot_memory = io.BytesIO(b"") # Method to get the dominant colour on screen. You can change this method to return whatever colour you like def get_dominant_colour() -> str: # Take a screenshot of the region specified earlier screenshot = ImageGrab.grab(screen_region) ''' The fast_colorthief library doesn't work directly with PIL images but we can use an in memory buffer (BytesIO) to store the picture This saves us writing then reading from the disk which is costly ''' # Save screenshot region to in-memory bytes buffer (instead of to disk) # Seeking and truncating fo performance rather than using "with" and creating/closing BytesIO object screenshot_memory.seek(0) screenshot_memory.truncate(0) screenshot.save(screenshot_memory, "PNG") # Get the dominant colour dominant_color = fast_colorthief.get_dominant_color(screenshot_memory, quality=1) # Return the colour in the form of hex (without the # prefix as our Bluetooth device doesn't use it) return '{:02x}{:02x}{:02x}'.format(*dominant_color)


El código está comentado, por lo que, con suerte, debería quedar claro lo que está sucediendo, pero estamos tomando una región más pequeña de la pantalla desde el medio y luego obteniendo el color dominante de esa región. La razón por la que elijo una región más pequeña es por el rendimiento; sería necesario analizar menos píxeles.


¡Casi estámos allí! Ahora sabemos qué enviarlo y dónde enviarlo. Terminemos la última parte importante de este desafío, que es enviarlo. Afortunadamente, con la biblioteca Bleak, esto es bastante sencillo.


 async def send_colour_to_device(client: BleakClient, uuid: str, value: str) -> None: #write to the characteristic we found, in the format that was obtained from the Bluetooth logs await client.write_gatt_char(uuid, bytes.fromhex(f"7e070503{value}10ef")) async def toggle_on(client: BleakClient, uuid: str) -> None: await client.write_gatt_char(uuid, bytes.fromhex(ON_HEX)) print("Turned on") async def toggle_off(client: BleakClient, uuid: str) -> None: await client.write_gatt_char(uuid, bytes.fromhex(OFF_HEX)) print("Turned off")


Como descubrimos en los registros, cada color tiene una plantilla fija, por lo que podemos usar f-strings para codificar la parte común y simplemente pasar un hexadecimal de un color para el valor en el medio. Esto se puede llamar desde nuestro bucle. On y Off tenían hexademicals únicos, así que creé funciones individuales y pasé un valor constante que contenía el hexadecimal relevante.


 while True: # send the dominant colour to the device await send_colour_to_device(client, characteristic_uuid, get_dominant_colour()) # allow a small amount of time before update time.sleep(0.1)


Y ahí lo tenemos; nuestras luces LED Bluetooth ahora están controladas por los colores en la pantalla, creando nuestro propio Monitor Ambiental.


Puede ver el código completo en GitHub , que tiene una pequeña cantidad de código de infraestructura que no era específico de esta publicación. He tratado de comentar el código para que se explique por sí mismo, pero siéntete libre de hacer cualquier pregunta o sugerencia.


Con suerte, esto le dará una idea de cómo puede comenzar a ser creativo con sus luces LED.

Mejoras futuras

  • Multicolor : mis tiras de luz solo pueden tener un color a la vez, pero tengo otro juego que puede tener cuatro cuadrantes, cada uno con sus propios colores. Esto significa que podría ser posible hacer que las secciones del monitor coincidan con cuatro secciones en la pantalla, lo que brinda una configuración de ambiente aún más precisa. Esas luces funcionan con Wifi en lugar de Bluetooth y podrían ser un proyecto futuro.
  • Brillo : para simplificar, solo busqué el cambio de color y los comandos de encendido y apagado. Sin embargo, esto se puede mejorar fácilmente detectando los comandos de control de brillo e incorporándolos al algoritmo de color.
  • Rendimiento : como queremos que las luces cambien en tiempo real, el rendimiento es fundamental. Existen algunos algoritmos complejos para detectar qué color se consideraría el más dominante, especialmente cuando lo perciben los humanos (lo que lleva a todo un mundo de conversiones de color). Sin embargo, dado que esto debe ejecutarse bastante rápido, debe haber un equilibrio entre el rendimiento y la precisión. Una mejora futura podría ser intentar acceder a la tarjeta gráfica directamente para leer desde el búfer en lugar de analizar los píxeles en la pantalla directamente. Si esto es posible, también eliminaría el tiempo que tarda el búfer de gráficos en la pantalla, lo que podría optimizar la reacción de las luces.


Siéntase libre de comentar a continuación si tiene algún comentario o pregunta.


También publicado aquí