Para aqueles que não estão familiarizados com as Ambient TVs, é uma maneira de suavizar o salto da borda da tela da TV e seus arredores imediatos para proporcionar uma experiência mais envolvente. Eu tinha algumas luzes de LED por aí e decidi ver se era possível controlar as luzes por meio de código e, por sua vez, tornar a tela do meu computador um monitor de ambiente. Embora eu quisesse usá-lo para o meu monitor, ele pode ser usado em qualquer lugar e com as cores que você enviar, incluindo outros recursos que suas luzes possam ter, como reações de áudio ou padrões aleatórios. Há algum tempo pretendo escrever este post, pois o uso em um monitor anterior, mas nunca consegui adicioná-lo ao meu novo monitor, então o documentei à medida que avançava para qualquer um que pudesse encontrar é útil. Então vamos fazer isso! (Observe que as luzes LED provavelmente são Bluetooth Low Energy (BLE), portanto, seu computador precisará oferecer suporte a BLE para interagir com elas). O código completo está no GitHub .
O primeiro passo que precisamos fazer é garantir que o aplicativo que acompanha as luzes esteja funcionando conforme o esperado. Isso pode ser facilmente testado executando o aplicativo original da luz e certificando-se de que as luzes reajam de acordo com os botões de ligar/desligar/iluminação que você está pressionando em seu aplicativo. Fazemos isso porque em breve estaremos pressionando e detectando os códigos específicos enviados ao receptor Bluetooth nas luzes.
Existem duas abordagens que eu poderia tomar. Uma delas era descompilar o arquivo JAR do app e encontrar os códigos que estavam sendo enviados, mas eu queria aprender mais sobre o protocolo Bluetooth, então optei por registrar todas as atividades do Bluetooth no meu Android e extrair de lá. Veja como:
Ative as opções do desenvolvedor no seu dispositivo Android.
Habilite o log de espionagem Bluetooth HCI (HCI significa Host-Controller Interface). Você pode encontrar essa opção em Configurações > Sistema > Desenvolvedor ou procurá-la nas configurações conforme a imagem abaixo.
Agora precisamos realizar ações específicas para que possamos identificar o que cada ação envia para o receptor Bluetooth da luz. Vou simplificar para Ligado/Vermelho/Verde/Azul/Desligado, nessa ordem, mas se suas luzes suportarem outros recursos, você também pode brincar com eles.
Execute o aplicativo e pressione On, Red, Green, Blue e Off. Também pode ser útil ficar de olho no tempo aproximado para facilitar a filtragem se você tiver muita atividade do Bluetooth no seu dispositivo.
Desligue o Bluetooth para não ouvirmos mais barulho. Nos passos seguintes, vamos analisar os comandos do Bluetooth e, como sabemos a ordem do que pressionamos, podemos saber quais valores correspondem a qual botão pressionado.
Agora precisamos acessar os logs do Bluetooth no telefone. Existem várias maneiras de fazer isso, mas vou gerar e exportar um relatório de bug. Para fazer isso, habilite a Depuração USB nas Configurações do telefone, conecte o telefone ao computador e use a ferramenta de linha de comando adb.exe.
adb bugreport led_bluetooth_report
Isso irá gerar um arquivo zip no diretório local do seu computador com o nome de arquivo “ led_bluetooth_report.zip ”. Você pode especificar um caminho se preferir (por exemplo, C:\MyPath\led_bluetooth_report”)
Dentro deste zip estão os logs que precisamos. Isso pode variar de dispositivo para dispositivo (por favor, comente se você o encontrou em outro lugar no seu dispositivo). No meu telefone Google Pixel, estava em FS\data\misc\bluetooth\logs\btsnoop_hci.log
Agora temos os arquivos de log, vamos analisá-los! Para fazer isso, decidi usar o Wireshark, então inicie o Wireshark e vá em File...Open... e selecione o arquivo de log btsnoop_hci.
Embora possa parecer assustador, vamos facilitar para nós mesmos encontrarmos o que estamos procurando, filtrando o BTL2CAP em 0x0004, que é o Protocolo de Atributo no código-fonte do Wireshark . O protocolo de atributo define a maneira como dois dispositivos BLE conversam entre si, então é disso que precisamos para ajudar a descobrir como o aplicativo se comunica com as luzes. Você pode filtrar os logs no Wireshark digitando btl2cap.cid == 0x0004 na barra “ Apply a display filter ” próximo ao topo e pressione Enter
Agora filtramos o log; deve facilitar a procura dos comandos. Podemos ver os carimbos de data e hora (Vá para Exibir…Formato de exibição de hora…Hora do dia para converter a hora se for o formato errado). Queremos ver os logs do Comando de Gravação Enviado, pois são aqueles para os quais enviamos um valor para as luzes. Supondo que seu horário mais recente esteja na parte inferior, role para baixo até os últimos cinco eventos. Eles devem ser On, Red, Green, Blue e Off nessa ordem, com Off sendo o último.
Anote o BD_ADDR de destino, pois precisaremos dele em breve, e coloque seu chapéu de Sherlock Holmes, pois é aqui que precisamos desvendar o padrão de como as cores e os comandos de ligar/desligar são codificados na mensagem. Isso varia dependendo do fabricante da luz, mas aqui está a lista de valores que obtive para o meu dispositivo:
Esses são claramente valores hexadecimais e, se você olhar com cuidado, verá que existem alguns padrões fixos. Vamos dividir os padrões, pois isso deve tornar as coisas muito mais claras.
Para aqueles familiarizados com valores hexadecimais de vermelho, verde e azul puros, você saberá que os valores são #FF000, #00FF00 e #0000FF, respectivamente, que é exatamente o que podemos ver acima. Isso significa que agora sabemos o formato para mudar as cores para o que quisermos! (ou pelo menos ao que as próprias luzes são capazes). Também podemos ver que On e Off têm um formato diferente das cores e são semelhantes entre si, sendo que On tem f00001 e Off tem 00000.
É isso! Agora temos informações suficientes para começar a codificar e interagir com as luzes.
Precisamos de três coisas essenciais:
O endereço do dispositivo (este é o BD_ADDR de destino acima)
Os valores a enviar para o dispositivo (os valores hexadecimais obtidos acima)
A característica que queremos mudar. Uma característica do Bluetooth LE é uma estrutura de dados que essencialmente define os dados que podem ser enviados entre um host e dispositivos Bluetooth do cliente. Precisamos encontrar a característica (um UUID de 16 bits ou 128 bits) que se refere às luzes. Existem alguns números atribuídos comumente usados que podem ser encontrados aqui, mas, a menos que o dispositivo esteja em conformidade com eles, eles podem estar usando um UUID personalizado. Como minhas luzes não estão na lista de números atribuídos, vamos encontrá-lo por meio do código.
Estou usando Python 3.10 e Bleak 0.20.1 . Certifique-se de que o Bluetooth do seu computador está ativado (não é necessário parear com o dispositivo, nos conectaremos a ele por meio de 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)
Eu comentei o código, então deve ser autoexplicativo, mas essencialmente, nos conectamos às luzes e encontramos todas as características que ela expõe. Minha saída foi:
Characteristic: 00002a00-0000-1000-8000-00805f9b34fb Characteristic: 00002a01-0000-1000-8000-00805f9b34fb Characteristic: 0000fff3-0000-1000-8000-00805f9b34fb Characteristic: 0000fff4-0000-1000-8000-00805f9b34fb
Um rápido Google dos dois primeiros UUIDs mostra que isso se refere ao nome e à aparência do serviço, o que é irrelevante para nós. No entanto, o terceiro e o quarto parecem os mais adequados, sendo o terceiro ( 0000fff3-0000-1000-8000-00805f9b34fb ) a característica de gravação de acordo com esta página . Excelente, agora temos a característica que precisamos para este determinado dispositivo escrever com um valor (a cor hexadecimal).
Finalmente temos todas as peças de que precisamos. Nesta fase, você pode ser criativo com a entrada de cor que gostaria de usar. Você poderia, por exemplo, conectar as luzes a uma API do mercado de negociação para alterar as cores de acordo com o desempenho do seu portfólio. Nesse caso, queremos tornar nossos monitores conscientes do ambiente, então precisamos obter a cor dominante da tela e enviá-la.
Há muitas maneiras de fazer isso, portanto, sinta-se à vontade para experimentar qualquer algoritmo que desejar. Uma das abordagens mais simples seria iterar cada número X de pixels na tela e obter uma média, enquanto soluções mais complicadas procurariam cores que os olhos humanos percebem como mais dominantes. Sinta-se livre para comentar sobre quaisquer descobertas que você gostaria de compartilhar!
Por causa desta postagem no blog, vou mantê-lo simples usando o método get_dominant_color da 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)
O código é comentado, portanto, deve ficar claro o que está acontecendo, mas estamos pegando uma região menor da tela a partir do meio e, em seguida, obtendo a cor dominante dessa região. O motivo pelo qual estou usando uma região menor é para desempenho; menos pixels precisariam ser analisados.
Estamos quase lá! Agora sabemos o que enviar e para onde enviar. Vamos terminar a última parte importante deste desafio, que é realmente enviá-lo. Felizmente, com a biblioteca Bleak, isso é bastante direto.
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 descobrimos nos logs, cada cor tem um modelo fixo, então podemos usar f-strings para codificar a parte comum e simplesmente passar um hexadecimal de uma cor para o valor no meio. Isso pode ser chamado de nosso loop. On e Off tinham hexadémicos únicos, então criei funções individuais e passei um valor constante que continha o 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)
E aí temos que; nossas luzes LED Bluetooth agora são controladas pelas cores na tela, criando nosso próprio Ambient Monitor.
Você pode ver o código completo no GitHub , que possui uma pequena quantidade de código de infraestrutura que não foi específico para esta postagem. Tentei comentar o código para ser autoexplicativo, mas fique à vontade para fazer qualquer pergunta ou sugestão.
Espero que isso lhe dê uma ideia de como você pode começar a ser criativo com suas luzes LED.
Sinta-se à vontade para comentar abaixo se tiver algum comentário ou pergunta.
Também publicado aqui