Ambient TV에 익숙하지 않은 분들을 위해 TV 화면 가장자리와 바로 주변 환경에서 점프하는 것을 부드럽게 만들어 더욱 몰입감 있는 경험을 제공하는 방법입니다. LED 조명이 몇 개 놓여 있었고 코드를 통해 조명을 제어하고 컴퓨터 화면을 주변 모니터로 만드는 것이 가능한지 확인하기로 결정했습니다. 모니터에 사용하고 싶었지만 오디오 반응이나 무작위 패턴과 같이 조명이 가질 수 있는 다른 기능을 포함하여 보낼 수 있는 모든 색상으로 어디에서나 사용할 수 있습니다. 이전 모니터에서 사용해왔기 때문에 한동안 이 글을 쓰려고 했는데 새 모니터에 추가할 여유가 없었기 때문에 혹시 모르시는 분들을 위해 기록해 두었습니다. 유용해요. 그럼 시작해 보겠습니다! (LED 조명은 BLE(Bluetooth Low Energy)일 가능성이 높으므로 LED 조명과 상호 작용하려면 컴퓨터가 BLE를 지원해야 합니다. 전체 코드는 GitHub 에 있습니다.
우리가 해야 할 첫 번째 단계는 조명과 함께 제공되는 앱이 예상대로 작동하는지 확인하는 것입니다. 이는 조명의 원래 앱을 실행하고 앱에서 누르고 있는 켜기/끄기/조명 버튼에 따라 조명이 적절하게 반응하는지 확인하여 쉽게 테스트할 수 있습니다. 우리는 곧 조명의 Bluetooth 수신기로 전송된 특정 코드를 누르고 감지할 것이기 때문에 이렇게 합니다.
제가 취할 수 있는 접근 방식은 두 가지입니다. 하나는 앱의 JAR 파일을 디컴파일하고 전송된 코드를 찾는 것이었지만 Bluetooth 프로토콜에 대해 더 자세히 알아보고 싶었기 때문에 Android의 모든 Bluetooth 활동을 기록하고 거기에서 추출하기로 결정했습니다. 방법은 다음과 같습니다.
Android 기기에서 개발자 옵션을 활성화하세요.
Bluetooth HCI 스누프 로그를 활성화합니다(HCI는 호스트 컨트롤러 인터페이스를 나타냄). 이 옵션은 설정 > 시스템 > 개발자 에서 찾거나 아래 이미지와 같이 설정에서 검색할 수 있습니다.
이제 각 작업이 조명의 Bluetooth 수신기로 보내는 내용을 식별할 수 있도록 특정 작업을 수행해야 합니다. On/Red/Green/Blue/Off 순서로 단순하게 유지하겠습니다. 하지만 조명이 다른 기능을 지원하는 경우 해당 기능도 가지고 놀 수 있습니다.
앱을 실행하고 On, Red, Green, Blue, Off를 누릅니다. 장치에서 Bluetooth 활동이 많은 경우 필터링을 더 쉽게 하기 위해 대략적인 시간을 계속 주시하는 것도 유용할 수 있습니다.
더 이상 소음이 들리지 않도록 블루투스를 꺼주세요. 다음 단계에서는 Bluetooth 명령을 분석하고 누른 순서를 알고 있으므로 어떤 값이 어떤 버튼을 누르는지 확인할 수 있습니다.
이제 전화기의 Bluetooth 로그에 액세스해야 합니다. 이를 수행하는 방법에는 여러 가지가 있지만 버그 보고서를 생성하고 내보내겠습니다. 이렇게 하려면 휴대폰 설정에서 USB 디버깅을 활성화하고 휴대폰을 컴퓨터에 연결한 다음 adb.exe 명령줄 도구를 사용하세요.
adb bugreport led_bluetooth_report
그러면 컴퓨터의 로컬 디렉터리에 파일 이름이 " led_bluetooth_report.zip "인 zip 파일이 생성됩니다. 원하는 경우 경로를 지정할 수 있습니다(예: C:\MyPath\led_bluetooth_report”).
이 zip 안에는 필요한 로그가 있습니다. 이는 기기마다 다를 수 있습니다(기기의 다른 곳에서 발견한 경우 댓글을 남겨주세요). 내 Google Pixel 휴대폰에서는 FS\data\misc\bluetooth\logs\btsnoop_hci.log 에 있었습니다.
이제 로그 파일이 있으므로 분석해 보겠습니다. 이를 위해 Wireshark를 사용하기로 결정했기 때문에 Wireshark를 시작하고 파일...열기...로 이동하여 btsnoop_hci 로그 파일을 선택합니다.
어렵게 보일 수도 있지만 Wireshark 소스 코드 의 속성 프로토콜인 0x0004 에서 BTL2CAP 를 필터링하여 원하는 것을 쉽게 찾을 수 있도록 하겠습니다. 속성 프로토콜은 두 개의 BLE 장치가 서로 통신하는 방식을 정의하므로 앱이 조명과 통신하는 방식을 찾는 데 도움이 필요합니다. 상단 근처의 " 디스플레이 필터 적용 " 표시줄에 btl2cap.cid == 0x0004 를 입력하고 Enter 키를 눌러 Wireshark의 로그를 필터링할 수 있습니다.
이제 로그를 필터링했습니다. 명령을 더 쉽게 찾을 수 있을 것입니다. 타임스탬프를 볼 수 있습니다(보기…시간 표시 형식…시간으로 이동하여 형식이 잘못된 경우 시간을 변환하세요). 우리는 Sent Write Command 로그를 살펴보고 싶습니다. 이는 조명에 값을 보낸 로그이기 때문입니다. 가장 최근 시간이 맨 아래에 있다고 가정하고 마지막 5개 이벤트까지 아래로 스크롤하세요. 켜짐, 빨간색, 녹색, 파란색, 꺼짐 순서로 표시되어야 하며 꺼짐이 마지막입니다.
곧 필요하므로 대상 BD_ADDR을 기록해 두고 Sherlock Holmes 모자를 쓰십시오. 여기에서 메시지 내에서 색상 및 켜기/끄기 명령이 인코딩되는 패턴을 잠금 해제해야 합니다. 이는 조명 제조업체에 따라 다르지만 내 장치에 대해 얻은 값 목록은 다음과 같습니다.
이는 분명히 16진수 값이며 주의 깊게 살펴보면 몇 가지 고정된 패턴이 있음을 알 수 있습니다. 패턴을 분리하면 상황이 훨씬 더 명확해집니다.
순수한 빨간색, 녹색, 파란색의 16진수 값에 익숙한 경우 값이 각각 #FF000, #00FF00 및 #0000FF라는 것을 알 수 있으며 이는 위에서 볼 수 있는 것과 정확히 같습니다. 이는 이제 우리가 원하는 색상으로 색상을 변경하는 형식을 알고 있음을 의미합니다! (또는 적어도 조명 자체가 할 수 있는 것까지). 또한 On과 Off는 색상과 다른 형식을 가지며 서로 유사하며 On에는 f00001이 있고 Off에는 00000이 있음을 알 수 있습니다.
그게 다야! 이제 코딩을 시작하고 조명과 상호 작용하는 데 충분한 정보가 있습니다.
우리에게 필요한 세 가지 핵심 사항은 다음과 같습니다.
장치의 주소(위의 대상 BD_ADDR임)
장치에 보낼 값(위에서 얻은 16진수 값)
우리가 바꾸고 싶은 특성. Bluetooth LE 특성은 호스트와 클라이언트 Bluetooth 장치 간에 전송될 수 있는 데이터를 기본적으로 정의하는 데이터 구조입니다. 조명을 참조하는 특성(16비트 또는 128비트 UUID)을 찾아야 합니다. 여기에서 찾을 수 있는 일반적으로 사용되는 할당 번호가 있지만 장치가 이를 준수하지 않는 한 사용자 정의 UUID를 사용할 수 있습니다. 내 조명은 할당된 번호 목록에 없으므로 코드를 통해 찾아보겠습니다.
저는 Python 3.10과 Bleak 0.20.1을 사용하고 있습니다. 컴퓨터의 Bluetooth가 켜져 있는지 확인하세요(장치와 페어링할 필요가 없으며 코드를 통해 연결됩니다).
# 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-1000-8000-00805f9b34fb 특성: 0000fff3-0000-1000-8000-00805f9b34fb 특성: 0000fff4-0000-1000-8000-00805f9b34fb
처음 두 개의 UUID에 대한 빠른 Google은 이것이 서비스의 이름과 모양을 의미하며 이는 우리와 관련이 없음을 보여줍니다. 그러나 이 페이지 에 따르면 세 번째와 네 번째가 쓰기 특성인 세 번째( 0000fff3-0000-1000-8000-00805f9b34fb )와 함께 가장 적합한 것 같습니다. 훌륭합니다. 이제 이 특정 장치에 값(16진수 색상)을 쓰는 데 필요한 특성이 생겼습니다.
마침내 우리는 필요한 모든 조각을 갖게 되었습니다. 이 단계에서는 어떤 색상 입력을 사용하고 싶은지 창의력을 발휘할 수 있습니다. 예를 들어 조명을 거래 시장 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-문자열을 사용하여 공통 부분을 하드코딩하고 중간 값에 대해 색상의 16진수를 전달하면 됩니다. 이는 루프에서 호출할 수 있습니다. On과 Off에는 고유한 16진수가 있으므로 개별 함수를 만들고 관련 16진수가 포함된 상수 값을 전달했습니다.
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)
그리고 거기에 우리가 있습니다; Bluetooth LED 조명은 이제 화면의 색상으로 제어되어 자체 주변 모니터를 만듭니다.
이 게시물과 관련이 없는 소량의 인프라 코드가 포함된 GitHub 에서 전체 코드를 볼 수 있습니다. 설명이 필요 없도록 코드에 주석을 달았지만 질문이나 제안 사항이 있으면 언제든지 문의해 주세요.
이것이 LED 조명으로 창의력을 발휘할 수 있는 방법에 대한 아이디어가 되기를 바랍니다.
피드백이나 질문이 있는 경우 아래에 자유롭게 의견을 남겨주세요.
여기에도 게시됨