8none1 / idealLED

Control your iDeal LED lights with Python
MIT License
51 stars 2 forks source link

Unable to change color of individual LEDs (all color-shift at the same time) #5

Open jmsiefer opened 3 days ago

jmsiefer commented 3 days ago

A giant THANK YOU for posting your code. I'm having a heck of a time just pulling a BT log, which should be the simplest of things, but whatever device I try it with just don't work for some reason.

  1. What device did you use in getting the BT traffic with?

  2. Any idea on how I get independently address the LEDs? I used the attached script to connect to the light strip, but I'm just unable to get single lights to change color, which I know works because the app will allow me to do it.

MUCH APPRECIATED!! -Josh

-----START CODE-----

import asyncio
import tkinter as tk
from tkinter import colorchooser
from bleak import BleakClient, BleakScanner
from Crypto.Cipher import AES
import threading

SECRET_ENCRYPTION_KEY = bytes([0x34, 0x52, 0x2A, 0x5B, 0x7A, 0x6E, 0x49, 0x2C,
                               0x08, 0x09, 0x0A, 0x9D, 0x8D, 0x2A, 0x23, 0xF8])

class LEDController:
    def __init__(self, address):
        self.address = address
        self.client = None

    async def connect(self):
        try:
            self.client = BleakClient(self.address)
            await self.client.connect()
            print(f"Connected to {self.address}")
        except Exception as e:
            print(f"Failed to connect: {e}")

    async def disconnect(self):
        if self.client and self.client.is_connected:
            await self.client.disconnect()
            print(f"Disconnected from {self.address}")

    async def set_rgb_color(self, red, green, blue):
        """Set a single color on the LED."""
        if not self.client or not self.client.is_connected:
            print("Not connected to the LED.")
            return

        packet = bytearray.fromhex("0F 53 47 4C 53 00 00 64 50 00 00 00 00 00 00 32")
        packet[9], packet[12] = red >> 3, red >> 3
        packet[10], packet[13] = green >> 3, green >> 3
        packet[11], packet[14] = blue >> 3, blue >> 3

        cipher = AES.new(SECRET_ENCRYPTION_KEY, AES.MODE_ECB)
        encrypted_packet = cipher.encrypt(packet)
        await self.client.write_gatt_char("d44bc439-abfd-45a2-b575-925416129600", encrypted_packet)

class App:
    def __init__(self, root):
        self.root = root
        self.led_controllers = {}
        self.selected_led = None
        self.loop = asyncio.new_event_loop()
        threading.Thread(target=self.run_event_loop, daemon=True).start()
        self.build_ui()

    def build_ui(self):
        self.root.title("LED Controller")

        self.listbox = tk.Listbox(self.root)
        self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.listbox.bind('<<ListboxSelect>>', self.on_led_select)

        tk.Button(self.root, text="Scan", command=self.scan_for_leds).pack(side=tk.TOP)
        tk.Button(self.root, text="Connect", command=self.connect_led).pack(side=tk.TOP)
        tk.Button(self.root, text="Disconnect", command=self.disconnect_led).pack(side=tk.TOP)
        tk.Button(self.root, text="Pick Color", command=self.pick_color).pack(side=tk.TOP)

    def on_led_select(self, event):
        selection = event.widget.curselection()
        if selection:
            index = selection[0]
            self.selected_led = self.listbox.get(index)

    def run_event_loop(self):
        asyncio.set_event_loop(self.loop)
        self.loop.run_forever()

    def scan_for_leds(self):
        asyncio.run_coroutine_threadsafe(self.scan_for_leds_async(), self.loop)

    async def scan_for_leds_async(self):
        devices = await BleakScanner.discover()
        for device in devices:
            if device.name:
                self.listbox.insert(tk.END, f"{device.name} ({device.address})")
                self.led_controllers[device.address] = LEDController(device.address)

    def connect_led(self):
        if self.selected_led:
            address = self.get_selected_led_address()
            controller = self.led_controllers[address]
            asyncio.run_coroutine_threadsafe(controller.connect(), self.loop)

    def disconnect_led(self):
        if self.selected_led:
            address = self.get_selected_led_address()
            controller = self.led_controllers[address]
            asyncio.run_coroutine_threadsafe(controller.disconnect(), self.loop)

    def pick_color(self):
        color = colorchooser.askcolor()[0]
        if color and self.selected_led:
            red, green, blue = map(int, color)
            address = self.get_selected_led_address()
            controller = self.led_controllers[address]
            asyncio.run_coroutine_threadsafe(
                controller.set_rgb_color(red, green, blue), self.loop
            )

    def get_selected_led_address(self):
        return self.selected_led.split('(')[-1].strip(')')

if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.mainloop()
8none1 commented 3 days ago

In order to set a single "pixel" you need to use what the app calls "Graffiti" mode.

https://github.com/8none1/idealLED/blob/d09838ccfd72f2cbb0e9367438b2a262ab63a8b4/idealled_controller.py#L89

You specify a pixel number and then it's RGB colour, as well as a "mode" (which from memory is 0,1,2 for steady, fade & flash - or something like that). Look in this file at the section which starts "in to graf mode" - the protocol is decoded there.

That should allow you to write an individual LED.

To reverse engineer the protocol I was using the Android debugging options built in to my phone, but Google are making it harder and harder to use for some reason. The old process is described in this blog post: https://www.whizzy.org/2023-12-14-bricked-xmas/

These days I find it waaaaay easier to use an nRF52840 dongle. They cost about $10 and integrate directly with Wireshark. It's really easy!

Check out this issue for some links to get you started if you're interested: https://github.com/8none1/idotmatrix/issues/1#issuecomment-2424862730