Interfaces

Group Assignment

I have explored a decent amount of interface and visualization tools from using processing to do simple edge detection , to javascript for my personal website, and computing libraries visualization tools: NumPy, matplotlib, Seaborn, Jax, Torch, Plotly, Triton, Manim. After comparing them, I like to use Torch / Jax for my ML work, and Matplotlib + Manim for visualization.

For this week I kept it simple and used my previous week’s output LEDS with Bluetooth Low Energy using the Pico W. I kept the same code running on the Pico W, and then I wanted to explore Tkinter. I have used it a long time ago, and I had the help of ChatGPT To make a simple interface with 3 buttons. https://chatgpt.com/share/6925c885-500c-8007-8dcc-07492a3903cb

import asyncio
import threading
import tkinter as tk
from bleak import BleakScanner, BleakClient

UART_RX = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"

client = None
status_label = None


async def ble_connect(loop):
    """Connect to PicoLED asynchronously, update GUI when done."""
    global client, status_label

    try:
        status_label.config(text="Scanning for PicoLED…")

        devices = await BleakScanner.discover()

        pico = None
        for d in devices:
            if d.name == "PicoLED":
                pico = d
                break

        if pico is None:
            status_label.config(text="PicoLED not found")
            return

        status_label.config(text=f"Found {pico.name}, connecting…")

        client = BleakClient(pico.address)
        await client.connect()

        status_label.config(text="Connected!")
        print("Connected to PicoLED!")

    except Exception as e:
        status_label.config(text=f"Error: {e}")


async def send_cmd(cmd):
    global client
    if client is None or not client.is_connected:
        print("Not connected!")
        status_label.config(text="Not connected")
        return

    await client.write_gatt_char(UART_RX, cmd.encode())
    print("Sent:", cmd)
    status_label.config(text=f"Sent '{cmd}'")


def run_asyncio(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()


def make_gui(loop):
    global status_label

    root = tk.Tk()
    root.title("PicoLED Controller")
    root.geometry("300x240")

    tk.Label(root, text="PicoLED Controller", font=("Arial", 18)).pack(pady=10)

    status_label = tk.Label(root, text="Connecting…", font=("Arial", 12))
    status_label.pack(pady=5)

    # Button helper
    def send(cmd):
        asyncio.run_coroutine_threadsafe(send_cmd(cmd), loop)

    tk.Button(root, text="Red", font=("Arial", 14), bg="red", fg="white",
              command=lambda: send("red")).pack(fill="x", padx=20, pady=5)

    tk.Button(root, text="Green", font=("Arial", 14), bg="green", fg="white",
              command=lambda: send("green")).pack(fill="x", padx=20, pady=5)

    tk.Button(root, text="Off", font=("Arial", 14), bg="#444", fg="white",
              command=lambda: send("off")).pack(fill="x", padx=20, pady=5)

    # Start connection AFTER GUI shows
    root.after(100, lambda: asyncio.run_coroutine_threadsafe(ble_connect(loop), loop))

    root.mainloop()


if __name__ == "__main__":
    loop = asyncio.new_event_loop()

    # background thread running asyncio
    t = threading.Thread(target=run_asyncio, args=(loop,), daemon=True)
    t.start()

    # main thread runs GUI
    make_gui(loop)

Let’s unpack this code. There are 2 key changes, first using threading. Threading allows for multiple processes to run “simultaneously” - I use quotes because if you just have 1 core, it needs to keep switching between threads. This is from my knowledge from my OS class in undergrad. So we will have one thread running the asynchronous BLE logic. Tkinter runs on the main thread, and when the button is pressed

tk.Button(root, text="Off", font=("Arial", 14), bg="#444", fg="white",
              command=lambda: send("off")).pack(fill="x", padx=20, pady=5)

We can see it sends a command

# Button helper
    def send(cmd):
        asyncio.run_coroutine_threadsafe(send_cmd(cmd), loop)

---

async def send_cmd(cmd):
    global client
    if client is None or not client.is_connected:
        print("Not connected!")
        status_label.config(text="Not connected")
        return

    await client.write_gatt_char(UART_RX, cmd.encode())
    print("Sent:", cmd)
    status_label.config(text=f"Sent '{cmd}'")


Here is a video of it working