paint-brush
Reverse Engineering LED Lights avec Python pour convertir votre écran d'ordinateur en un moniteur ambiantpar@reaminated
2,021 lectures
2,021 lectures

Reverse Engineering LED Lights avec Python pour convertir votre écran d'ordinateur en un moniteur ambiant

par Reaminated12m2023/03/31
Read on Terminal Reader

Trop long; Pour lire

Convertissez votre moniteur normal en moniteur ambiant avec des lumières changeant pour s'adapter aux couleurs actuellement à l'écran. À l'aide de Python et de lumières RVB, il s'agit d'un didacticiel pour montrer comment améliorer votre expérience de film et de jeu.
featured image - Reverse Engineering LED Lights avec Python pour convertir votre écran d'ordinateur en un moniteur ambiant
Reaminated HackerNoon profile picture
0-item

Introduction

Pour ceux qui ne sont pas familiers avec les téléviseurs ambiants, c'est un moyen d'adoucir le saut du bord de l'écran du téléviseur et de son environnement immédiat pour offrir une expérience plus immersive. J'avais des lumières LED qui traînaient et j'ai décidé de voir s'il était possible de contrôler les lumières par code et, à son tour, de faire de mon écran d'ordinateur un moniteur ambiant. Alors que je voulais l'utiliser pour mon moniteur, il peut être utilisé n'importe où et avec toutes les couleurs que vous pouvez lui envoyer, y compris d'autres fonctionnalités que vos lumières peuvent avoir, telles que des réactions audio ou des motifs aléatoires. Je voulais écrire ce post depuis un moment car je l'utilisais sur un moniteur précédent, mais je n'ai jamais eu le temps de l'ajouter à mon nouveau moniteur, alors je l'ai documenté au fur et à mesure pour tous ceux qui pourraient trouver c'est utile. Alors allons-y ! (Veuillez noter que les lumières LED sont susceptibles d'être Bluetooth Low Energy (BLE), donc votre ordinateur devra prendre en charge BLE pour interagir avec eux). Le code complet est sur GitHub .

Étapes de haut niveau

  • Découvrez quelles commandes le récepteur Bluetooth de la lumière LED accepte
  • Envoyer des commandes aux lumières LED via le Bluetooth de mon ordinateur
  • Obtenir la couleur dominante de l'écran actuel
  • Envoyez la couleur dominante aux lumières LED

Conditions préalables

  • Lumières LED RVB prises en charge par Bluetooth et application associée (j'utilise Android, iOS nécessiterait probablement une approche alternative à celle décrite ici, mais il devrait être possible d'utiliser Wireshark directement pour surveiller le trafic Bluetooth). J'ai attaché ces lumières à l'arrière de mon moniteur
  • Wireshark
  • Outils SDK d'Android (en particulier adb.exe)
  • Outils de développement (j'utiliserai Python 3.10, bien que toutes les versions 3.x devraient fonctionner, mais les principes devraient être les mêmes quel que soit le langage que vous préférez)
  • Un appareil à partir duquel envoyer des commandes BLE (par exemple, un ordinateur portable prenant en charge BLE)


Obtenir des données Bluetooth

La première étape que nous devons faire est de nous assurer que l'application fournie avec les lumières fonctionne comme prévu. Cela peut facilement être testé en exécutant l'application d'origine de la lumière et en s'assurant que les lumières réagissent en conséquence en fonction des boutons marche/arrêt/éclairage sur lesquels vous appuyez sur votre application. Nous le faisons parce que nous allons bientôt appuyer et détecter les codes spécifiques envoyés au récepteur Bluetooth sur les lumières.


Il y a deux approches que je pourrais adopter. L'une consistait à décompiler le fichier JAR de l'application et à trouver les codes qui étaient envoyés, mais je voulais en savoir plus sur le protocole Bluetooth, j'ai donc choisi de consigner toutes les activités Bluetooth sur mon Android et de les extraire de là. Voici comment:


  1. Activez les options de développeur sur votre appareil Android.


  2. Activer le journal de surveillance Bluetooth HCI (HCI signifie Host-Controller Interface). Vous pouvez trouver cette option dans Paramètres> Système> Développeur ou la rechercher dans les paramètres comme dans l'image ci-dessous.


Activation des journaux de surveillance HCI


  1. Nous devons maintenant effectuer des actions spécifiques afin de pouvoir identifier ce que chaque action envoie au récepteur Bluetooth de la lumière. Je vais rester simple sur Marche/Rouge/Vert/Bleu/Arrêt, dans cet ordre, mais si vos lumières prennent en charge d'autres fonctionnalités, vous pouvez également jouer avec celles-ci.


  2. Exécutez l'application et appuyez sur Marche, Rouge, Vert, Bleu et Arrêt. Il peut également être utile de garder un œil sur l'heure approximative pour faciliter le filtrage si vous avez beaucoup d'activité Bluetooth sur votre appareil.


Enregistrement des commandes à envoyer


  1. Désactivez le Bluetooth pour ne plus avoir de bruit. Dans les étapes suivantes, nous analyserons les commandes Bluetooth et, comme nous connaissons l'ordre de ce que nous avons pressé, nous pouvons savoir quelles valeurs correspondent à quel bouton pressé.


  2. Nous devons maintenant accéder aux journaux Bluetooth sur le téléphone. Il existe plusieurs façons de procéder, mais je vais générer et exporter un rapport de bogue. Pour ce faire, activez le débogage USB dans les paramètres du téléphone, connectez le téléphone à l'ordinateur et utilisez l'outil de ligne de commande adb.exe.


     adb bugreport led_bluetooth_report


  3. Cela générera un fichier zip dans le répertoire local de votre ordinateur avec le nom de fichier « led_bluetooth_report.zip ». Vous pouvez spécifier un chemin si vous préférez (par exemple, C:\MyPath\led_bluetooth_report")


  4. Dans ce zip se trouvent les journaux dont nous avons besoin. Cela peut varier d'un appareil à l'autre (veuillez commenter si vous l'avez trouvé ailleurs sur votre appareil). Sur mon téléphone Google Pixel, c'était dans FS\data\misc\bluetooth\logs\btsnoop_hci.log


  5. Maintenant que nous avons les fichiers journaux, analysons-les ! Pour ce faire, j'ai décidé d'utiliser Wireshark alors démarrez Wireshark et allez dans Fichier... Ouvrir... et sélectionnez le fichier journal btsnoop_hci.


Bien que cela puisse sembler intimidant, facilitons-nous la tâche pour trouver ce que nous recherchons en filtrant le BTL2CAP sur 0x0004, qui est le protocole d'attribut dans le code source de Wireshark . Le protocole d'attribut définit la façon dont deux appareils BLE se parlent, c'est donc ce dont nous avons besoin pour aider à trouver comment l'application parle aux lumières. Vous pouvez filtrer les journaux dans Wireshark en tapant btl2cap.cid == 0x0004 dans la barre " Appliquer un filtre d'affichage " en haut et appuyez sur Entrée


Filtrez les logs pour vous faciliter la tâche


Nous avons maintenant filtré le journal ; cela devrait faciliter la recherche des commandes. Nous pouvons regarder les horodatages (Aller à Affichage…Format d'affichage de l'heure…Heure de la journée pour convertir l'heure si c'est le mauvais format). Nous voulons examiner les journaux des commandes d'écriture envoyées car ce sont ceux où nous avons envoyé une valeur aux lumières. En supposant que votre heure la plus récente se trouve en bas, faites défiler jusqu'aux cinq derniers événements. Celles-ci doivent être activées, rouges, vertes, bleues et désactivées dans cet ordre, la désactivation étant la dernière.


BS_ADDR et Valeurs - Les informations dont nous avons besoin !


Prenez note de la destination BD_ADDR, car nous en aurons besoin sous peu, et mettez votre chapeau Sherlock Holmes, car c'est là que nous devons déverrouiller le modèle d'encodage des couleurs et des commandes marche/arrêt dans le message. Cela variera selon le fabricant de lumière, mais voici la liste des valeurs que j'ai obtenues pour mon appareil :


  • Sur : 7e0404f00001ff00ef
  • Rouge : 7e070503ff000010ef
  • Vert : 7e07050300ff0010ef
  • Bleu : 7e0705030000ff10ef
  • Désactivé : 7e0404000000ff00ef


Ce sont clairement des valeurs hexadécimales et si vous regardez attentivement, vous verrez qu'il existe des modèles fixes. Séparons les modèles car cela devrait rendre les choses beaucoup plus claires.


  • Sur : 7e0404 f00001 ff00ef
  • Rouge : 7e070503 ff0000 10ef
  • Vert : 7e070503 00ff00 10ef
  • Bleu : 7e070503 0000ff 10ef
  • Désactivé : 7e0404 000000 ff00ef


Pour ceux qui connaissent les valeurs hexadécimales de rouge, vert et bleu purs, vous saurez que les valeurs sont #FF000, #00FF00 et #0000FF, respectivement, ce qui est exactement ce que nous pouvons voir ci-dessus. Cela signifie que nous connaissons maintenant le format pour changer les couleurs en ce que nous voulons ! (ou du moins à ce dont les lumières elles-mêmes sont capables). Nous pouvons également voir que On et Off ont un format différent des couleurs et sont similaires les uns aux autres, On ayant f00001 et Off ayant 00000.

C'est ça! Nous avons maintenant suffisamment d'informations pour commencer à coder et interagir avec les lumières.

Connexion aux lumières LED

Il y a trois éléments clés dont nous avons besoin :


  • L'adresse de l'appareil (il s'agit de la destination BD_ADDR ci-dessus)

  • Les valeurs à envoyer à l'appareil (les valeurs hexadécimales obtenues ci-dessus)

  • La caractéristique que nous voulons changer. Une caractéristique Bluetooth LE est une structure de données qui définit essentiellement les données pouvant être envoyées entre un hôte et des appareils Bluetooth clients. Nous devons trouver la caractéristique (un UUID 16 bits ou 128 bits) qui fait référence aux lumières. Certains numéros attribués couramment utilisés peuvent être trouvés ici, mais à moins que l'appareil ne soit conforme à ceux-ci, ils pourraient utiliser un UUID personnalisé. Comme mes lumières ne sont pas dans la liste des numéros attribués, retrouvons-les via le code.


J'utilise Python 3.10 et Bleak 0.20.1 . Assurez-vous que le Bluetooth sur votre ordinateur est activé (inutile de coupler l'appareil, nous nous y connecterons via un code).


 # 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)


J'ai commenté le code, il devrait donc être explicite mais essentiellement, nous nous connectons aux lumières et trouvons toutes les caractéristiques qu'il expose. Ma sortie était:


Caractéristique: 00002A00-0000-1000-8000-00805F9B34FB Caractéristique: 00002A01-0000-1000-8000-00805F9B34FB Caractéristique: 0000FFF3-0000-1000-8000-00805F9B34FB Caractéristique: 0000FFF4-0000-1000-8000-00805


Une recherche rapide sur Google des deux premiers UUID montre que cela fait référence au nom et à l'apparence du service, ce qui n'est pas pertinent pour nous. Cependant, le troisième et le quatrième semblent les plus adaptés, le troisième ( 0000fff3-0000-1000-8000-00805f9b34fb ) étant la caractéristique d'écriture selon cette page . Excellent, nous avons maintenant la caractéristique dont nous avons besoin pour que cet appareil particulier écrive avec une valeur (la couleur hexadécimale).

Contrôle des lumières LED

Nous avons enfin toutes les pièces dont nous avons besoin. À ce stade, vous pouvez faire preuve de créativité avec la couleur que vous souhaitez utiliser. Vous pouvez, par exemple, connecter les lumières à une API de marché de trading pour changer les couleurs en fonction de l'évolution de votre portefeuille. Dans ce cas, nous voulons rendre nos moniteurs conscients de l'ambiance, nous devons donc obtenir la couleur dominante de l'écran et l'envoyer.


Il existe de nombreuses façons de le faire, alors n'hésitez pas à expérimenter avec les algorithmes que vous souhaitez. L'une des approches les plus simples serait d'itérer chaque nombre X de pixels sur l'écran et de prendre une moyenne tandis que des solutions plus compliquées chercheraient les couleurs que les yeux humains perçoivent comme étant plus dominantes. N'hésitez pas à commenter les découvertes que vous aimeriez partager !

Pour les besoins de cet article de blog, je vais rester simple en utilisant la méthode get_dominant_color de la bibliothèque 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)


Le code est commenté, donc j'espère qu'il devrait être clair quant à ce qui se passe, mais nous prenons une plus petite région de l'écran à partir du milieu, puis nous obtenons la couleur dominante de cette région. La raison pour laquelle je prends une région plus petite est pour les performances ; moins de pixels devraient être analysés.


Nous y sommes presque! Nous savons maintenant quoi envoyer et où l'envoyer. Terminons la dernière grande partie de ce défi qui consiste à l'envoyer réellement. Heureusement, avec la bibliothèque Bleak, c'est assez simple.


 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")


Comme nous l'avons découvert dans les journaux, chaque couleur a un modèle fixe, nous pouvons donc utiliser des chaînes f pour coder en dur la partie commune et simplement passer un hexadécimal d'une couleur pour la valeur au milieu. Cela peut être appelé depuis notre boucle. On et Off avaient des hexadécimaux uniques, j'ai donc créé des fonctions individuelles et transmis une valeur constante contenant l'hexadécimal pertinent.


 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)


Et voilà, nous l'avons ; nos lumières LED Bluetooth sont maintenant contrôlées par les couleurs sur l'écran, créant notre propre moniteur ambiant.


Vous pouvez voir le code complet sur GitHub , qui contient une petite quantité de code d'infrastructure qui n'était pas spécifique à cet article. J'ai essayé de commenter le code pour qu'il soit explicite, mais n'hésitez pas à poser des questions ou à faire des suggestions.


J'espère que cela vous donne une idée de la façon dont vous pouvez commencer à faire preuve de créativité avec vos lumières LED.

Améliorations futures

  • Multicolore - Mes bandes lumineuses ne peuvent avoir qu'une seule couleur à la fois, mais j'ai un autre ensemble qui peut avoir quatre quadrants, chacun avec ses propres couleurs. Cela signifie qu'il pourrait être possible d'avoir des sections du moniteur correspondant à quatre sections à l'écran, ce qui donne un réglage d'ambiance encore plus précis. Ces lumières fonctionnent sur Wifi au lieu de Bluetooth et pourraient être un futur projet.
  • Luminosité - pour faire simple, j'ai juste cherché le changement de couleur et les commandes marche-arrêt. Cependant, cela peut facilement être amélioré en détectant les commandes de contrôle de la luminosité et en les intégrant à l'algorithme de couleur.
  • Performance - Comme nous voulons que les lumières changent en temps réel, les performances sont essentielles. Il existe des algorithmes complexes pour détecter quelle couleur serait considérée comme la plus dominante, en particulier lorsqu'elle est perçue par les humains (ce qui conduit à tout un monde de conversions de couleurs). Cependant, comme cela doit fonctionner assez rapidement, il doit y avoir un équilibre entre performances et précision. Une amélioration future pourrait être d'essayer d'accéder directement à la carte graphique pour lire à partir du tampon plutôt que d'analyser directement les pixels à l'écran. Si cela est possible, vous éliminerez également le temps passé entre le tampon graphique et l'écran, ce qui pourrait optimiser la réaction des lumières.


N'hésitez pas à commenter ci-dessous si vous avez des commentaires ou des questions.


Également publié ici