paint-brush
使用 Python 对 LED 灯进行逆向工程,将您的计算机显示器转换为环境显示器经过@reaminated
2,352 讀數
2,352 讀數

使用 Python 对 LED 灯进行逆向工程,将您的计算机显示器转换为环境显示器

经过 Reaminated12m2023/03/31
Read on Terminal Reader

太長; 讀書

将您的普通显示器转换为环境显示器,灯光会随着屏幕上当前的颜色而变化。这是一个使用 Python 和 RGB 灯的教程,展示如何增强您的电影和游戏体验。
featured image - 使用 Python 对 LED 灯进行逆向工程,将您的计算机显示器转换为环境显示器
Reaminated HackerNoon profile picture
0-item

介绍

对于那些不熟悉 Ambient TV 的人来说,这是一种软化电视屏幕边缘及其周围环境的跳动以提供更身临其境的体验的方法。我周围有一些 LED 灯,我决定看看是否可以通过代码控制这些灯,进而让我的电脑屏幕成为环境监视器。虽然我想将它用于我的显示器,但它可以在任何地方使用,并且可以发送任何颜色,包括您的灯可能具有的其他功能,例如音频反应或随机模式。我一直想写这篇文章,因为我一直在早期的显示器上使用它,但我从来没有抽出时间将它添加到我的新显示器上,所以我在使用过程中记录了它,以供可能发现的任何人使用它有用。让我们开始吧! (请注意,LED 灯很可能是低功耗蓝牙 (BLE),因此您的计算机需要支持 BLE 才能与它们交互)。完整代码在GitHub上。

高级步骤

  • 查看 LED 灯的蓝牙接收器接受哪些命令
  • 通过我电脑的蓝牙向 LED 灯发送命令
  • 获取当前屏幕的主色
  • 将主色发送到 LED 灯

先决条件

  • 蓝牙支持的 RGB LED 灯和随附的应用程序(我使用的是 Android,iOS 可能需要一种不同于此处描述的方法,但应该可以直接使用 Wireshark 来监控蓝牙流量)。我已将这些灯连接到我的显示器背面
  • 线鲨
  • Android的SDK工具(具体是adb.exe)
  • 开发人员工具(我将使用 Python 3.10,尽管任何 3.x 版本都应该可以使用,但原则应该与您喜欢的语言相同)
  • 发送 BLE 命令的设备(例如支持 BLE 的笔记本电脑)


获取蓝牙数据

我们需要做的第一步是确保灯附带的应用程序按预期工作。这可以通过运行灯的原始应用程序并确保灯根据您在应用程序上按下的开/关/照明按钮做出相应反应来轻松测试。我们这样做是因为我们很快就会按下并检测发送到灯上蓝牙接收器的特定代码。


我可以采取两种方法。一个是反编译应用程序的 JAR 文件并找到正在发送的代码,但我想了解更多关于蓝牙协议的信息,所以我选择记录我 Android 上的所有蓝牙活动并从那里提取它。就是这样:


  1. 在您的 Android 设备上启用开发者选项


  2. 启用蓝牙 HCI 监听日志(HCI 代表主机控制器接口)。您可以在“设置”>“系统”>“开发人员”中找到此选项,或在设置中搜索它,如下图所示。


启用 HCI 侦听日志


  1. 我们现在需要执行特定的操作,以便我们可以识别每个操作发送到灯的蓝牙接收器的内容。我将按照开/红/绿/蓝/关的顺序保持简单,但如果你的灯支持其他功能,你也可以玩弄那些。


  2. 运行应用程序,然后按开、红、绿、蓝和关。如果您的设备上有很多蓝牙活动,那么关注大概时间也可能会有用,这样可以更轻松地进行过滤。


录制要发送的命令


  1. 关闭蓝牙,这样我们就不会再听到任何噪音。在接下来的步骤中,我们将分析蓝牙命令,因为我们知道按下的顺序,我们可以找出哪些值对应于哪个按钮按下。


  2. 我们现在需要访问手机上的蓝牙日志。有几种方法可以做到这一点,但我将生成并导出错误报告。为此,请在手机的设置中启用 USB 调试,将手机连接到计算机,然后使用 adb.exe 命令行工具。


     adb bugreport led_bluetooth_report


  3. 这将在您计算机的本地目录中生成一个 zip 文件,文件名为“ led_bluetooth_report.zip ”。如果您愿意,可以指定路径(例如 C:\MyPath\led_bluetooth_report”)


  4. 在这个 zip 中是我们需要的日志。这可能因设备而异(如果您在设备的其他地方找到它,请发表评论)。在我的 Google Pixel 手机上,它位于FS\data\misc\bluetooth\logs\btsnoop_hci.log


  5. 现在我们有了日志文件,让我们来分析它们!为此,我决定使用 Wireshark,因此启动 Wireshark 并转到文件...打开...并选择 btsnoop_hci 日志文件。


虽然它可能看起来令人生畏,但让我们通过过滤0x0004上的BTL2CAP来轻松找到我们正在寻找的东西,这是Wireshark 源代码中的属性协议。属性协议定义了两个 BLE 设备相互通信的方式,所以这就是我们需要帮助找到应用程序如何与灯通信的方式。您可以通过在靠近顶部的“应用显示过滤器”栏中键入btl2cap.cid == 0x0004并按 Enter 来过滤 Wireshark 中的日志


过滤日志以使其更容易


现在我们已经过滤了日志;它应该使查找命令更容易。我们可以查看时间戳(如果格式错误,请转到查看...时间显示格式...时间以转换时间)。我们想查看 Sent Write Command 日志,因为它们是我们向灯发送值的地方。假设您最近的时间在底部,向下滚动到最后五个事件。这些应该依次是开、红、绿、蓝和关,关在最后。


BS_ADDR 和值——我们需要的信息!


记下 Destination BD_ADDR,因为我们很快就会需要它,然后戴上你的 Sherlock Holmes 帽子,因为这是我们需要解锁消息中颜色和开/关命令编码方式的地方。这将因灯制造商而异,但这是我为我的设备获得的值列表:


  • 在:7e0404f00001ff00ef
  • 红色:7e070503ff000010ef
  • 绿色:7e07050300ff0010ef
  • 蓝色:7e0705030000ff10ef
  • 关闭:7e0404000000ff00ef


这些显然是十六进制值,如果仔细观察,您会发现有一些固定的模式。让我们把模式分开,因为这会让事情变得更清楚。


  • 打开:7e0404 f00001 ff00ef
  • 红色:7e070503 ff0000 10ef
  • 绿色:7e070503 00ff00 10ef
  • 蓝色:7e070503 0000ff 10ef
  • 关闭:7e0404 000000 ff00ef


熟悉纯红、纯绿、纯蓝的十六进制值的人会知道,这些值分别是#FF000、#00FF00、#0000FF,这正是我们在上面看到的。这意味着我们现在知道将颜色更改为我们想要的任何格式! (或者至少是灯光本身的能力)。我们还可以看到 On 和 Off 的格式与颜色不同,但彼此相似,On 具有 f00001,Off 具有 00000。

就是这样!我们现在有足够的信息开始编码并与灯交互。

连接到 LED 灯

我们需要三个关键的东西:


  • 设备地址(这是上面的 Destination BD_ADDR)

  • 要发送到设备的值(上面获得的十六进制值)

  • 我们想要改变的特征。 Bluetooth LE 特征是一种数据结构,它本质上定义了可以在主机和客户端蓝牙设备之间发送的数据。我们需要找到与灯有关的特征(16 位或 128 位 UUID)。可以在此处找到一些常用的分配编号,但除非设备符合这些编号,否则它们可能会使用自定义 UUID。由于我的灯不在分配的编号列表中,让我们通过代码找到它。


我正在使用 Python 3.10 和Bleak 0.20.1 。确保计算机上的蓝牙已打开(无需与设备配对,我们将通过代码连接到它)。


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


我已经对代码进行了评论,所以它应该是不言自明的,但本质上,我们连接到灯并找到它暴露的所有特征。我的输出是:


特征:00002A00-0000-1000-8000-00805F9B34FB特征:00002A01-0000-0000-0000-1000-8000-8000-00805F9B34FB特征:000000FF3-00000000-0000-1000-8000-8000-0080-00805F9B34FB特征:


对前两个 UUID 的快速谷歌显示这是指服务的名称和外观,这与我们无关。但是,第三个和第四个似乎最合适,第三个( 0000fff3-0000-1000-8000-00805f9b34fb )是根据此页面的写入特性。太棒了,我们现在有了这个特定设备需要的特征来写入一个值(颜色十六进制)。

控制 LED 灯

我们终于拥有了我们需要的所有零件。在此阶段,您可以根据自己喜欢的颜色输入来发挥创意。例如,您可以将灯连接到交易市场 API,以根据您的投资组合的表现改变颜色。在这种情况下,我们想让我们的显示器感知环境,因此我们需要获取屏幕的主色并将其发送出去。


有很多方法可以做到这一点,所以请随意尝试您喜欢的任何算法。最简单的方法之一是在屏幕上迭代每 X 个像素并取平均值,而更复杂的解决方案将寻找人眼感知的颜色更占主导地位。随时对您想分享的任何发现发表评论!

为了这篇博文,我将通过使用fast_colorthief库的 get_dominant_color 方法来保持简单。


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


代码已被注释,所以希望它应该清楚发生了什么,但我们从屏幕中间取一个较小的区域,然后从该区域获取主色。我选择较小区域的原因是为了性能;需要分析的像素更少。


我们快到了!我们现在知道发送什么以及发送到哪里。让我们完成这个挑战的最后一个主要部分,即实际发送它。幸运的是,有了 Bleak 库,这非常简单。


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


正如我们从日志中发现的那样,每种颜色都有一个固定的模板,因此我们可以使用 f-strings 对公共部分进行硬编码,并简单地传递一个颜色的十六进制作为中间值。这可以从我们的循环中调用。 On 和 Off 具有独特的十六进制,因此我创建了单独的函数并传入了一个包含相关十六进制的常量值。


 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)


我们终于得到它了;我们的蓝牙 LED 灯现在由屏幕上的颜色控制,创建了我们自己的环境监视器。


您可以在GitHub上查看完整代码,其中包含少量并非特定于本文的基础结构代码。我试图将代码注释为不言自明,但请随时提出任何问题或提出建议。


希望这能让您了解如何开始使用 LED 灯发挥创意。

未来的改进

  • 多色——我的灯条一次只能有一种颜色,但我有另一组可以有四个象限,每个象限都有自己的颜色。这意味着可以让显示器的部分与屏幕上的四个部分相匹配,从而提供更准确的氛围设置。这些灯在 Wifi 而不是蓝牙上运行,可能是未来的项目。
  • 亮度——为简单起见,我只寻找颜色变化和开关命令。然而,这可以通过检测亮度控制命令并将其放入颜色算法中轻松改进。
  • 性能- 由于我们想让灯光实时变化,因此性能至关重要。有一些复杂的算法可以检测哪种颜色被认为是最主要的,尤其是当被人类感知时(这会导致整个世界的颜色转换)。但是,由于这需要运行得相当快,因此需要在性能和准确性之间取得平衡。未来的改进可能是尝试直接访问图形卡以从缓冲区读取数据,而不是直接分析屏幕上的像素。如果可能的话,您还可以消除从图形缓冲区到屏幕所花费的时间,这可以优化灯光的反应。


如果您有任何反馈或问题,请随时在下方发表评论。


也在这里发布