Reverse Engineering einer Bluetooth-gesteuerten 6€ LED-Leiste

David Vierkötter am 15.06.2025 | Beitrags-ID: 145 1 Kommentar

LED-Beleuchtung im Serverschrank bietet nicht nur ein optisches Upgrade, sondern kann auch funktionale Vorteile wie Statusanzeigen oder einfachere Wartung bringen. Besonders interessant wird es, wenn handelsübliche, günstige Komponenten in ein professionelles System integriert werden – so wie im folgenden Projekt, bei dem eine 6-Euro-LED-Leiste mit Bluetooth-Ansteuerung vollständig lokal steuerbar gemacht wurde.

Reverse Engineering der Bluetooth-Kommunikation

Die mitgelieferte Fernbedienung der LED-Leiste funktionierte zwar, doch die Bluetooth-Funktionalität war exklusiv an die proprietäre "illumi-home"-App gebunden – ein No-Go für ein kontrolliertes Homelab. Um die Kommunikation zu analysieren, wurde zunächst versucht, den Datenverkehr auf iOS mit nRF Connect zu sniffen – allerdings scheiterte dieser Ansatz an Systembeschränkungen. Auch Android erwies sich als wenig hilfreich. Die Lösung: Der Einsatz von PacketLogger im macOS-Entwicklermodus. Mit einem aktivierten Bluetooth-Debug-Profil konnten die relevanten Protokollinhalte identifiziert werden: Handshake-Kommandos, Keep-Alive-Signale und Farbsteuerbefehle.

Effiziente Steuerung durch Python-Service

Ziel war die direkte Steuerung über ein Python-Skript auf einem NUC-Server. Erste Tests offenbarten jedoch eine störende Latenz, verursacht durch die wiederholte Verbindungsaufnahme bei jedem einzelnen Farbwechsel. Abhilfe schuf eine persistente Client-Server-Architektur: Ein kontinuierlich laufender Service-Worker hielt die Bluetooth-Verbindung offen, während ein leichtgewichtiges Client-Skript lediglich Befehle sendete. Zusätzliche Stabilität brachte der Austausch des internen NUC-Bluetooth-Moduls gegen einen externen USB-Adapter mit CSR-Chip – seitdem funktioniert die Verbindung auch bei kurzen Distanzen zuverlässig.

Service-Worker
#!/usr/bin/env python3
import asyncio
import os
import sys
import socket
from bleak import BleakClient, BleakError
from datetime import datetime
from typing import Optional

# Konfiguration
DEVICE_ADDRESS = "00:00:00:00:00:00" #BT-Adresse der LED-Leiste
CHAR_UUID = "00000000-0000-0000-0000-000000000000" #UUID der Write-Befehle (kann einfach über nRF-Connect ausgelesen werden)
SOCKET_PATH = "/tmp/led_ble.sock"
KEEPALIVE_COMMAND = bytes.fromhex("5A0102FF")
ADAPTER_NAME = "hci0"  #BT Adapter-Name des NUCs

# Optimierte Intervalle
KEEPALIVE_INTERVAL = 60
BLE_OPERATION_TIMEOUT = 5
CONNECTION_TIMEOUT = 8

# Retry-Logik
INITIAL_RETRY_DELAY = 1
MAX_RETRY_DELAY = 15
MAX_RETRIES_BEFORE_RESET = 2

class Colors:
    GREEN = "�33[92m"
    YELLOW = "�33[93m"
    RED = "�33[91m"
    BLUE = "�33[94m"
    RESET = "�33[0m"

def log(message: str, level: str = "info"):
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
    color = {
        "info": Colors.GREEN,
        "warning": Colors.YELLOW,
        "error": Colors.RED,
        "debug": Colors.BLUE
    }.get(level.lower(), Colors.RESET)
    print(f"{color}[{timestamp}] {message}{Colors.RESET}")

async def handle_socket(client: BleakClient, stop_event: asyncio.Event):
    if os.path.exists(SOCKET_PATH):
        os.remove(SOCKET_PATH)

    server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    server.setblocking(False)
    server.bind(SOCKET_PATH)
    server.listen(1)
    log("???? Warte auf Befehle via Socket", "info")

    try:
        while not stop_event.is_set():
            try:
                conn, _ = await asyncio.get_event_loop().sock_accept(server)
                with conn:
                    data = await asyncio.get_event_loop().sock_recv(conn, 1024)
                    if not data:
                        continue

                    try:
                        payload = bytes.fromhex(data.decode().strip())
                        await client.write_gatt_char(CHAR_UUID, payload)
                        log(f"✅ Befehl gesendet: {data.decode().strip()}", "info")
                    except Exception as e:
                        log(f"❌ Fehler beim Senden: {e}", "error")
            except Exception as e:
                if not stop_event.is_set():
                    log(f"⚠️  Socket Fehler: {e}", "warning")
                await asyncio.sleep(0.5)
    finally:
        server.close()
        if os.path.exists(SOCKET_PATH):
            os.remove(SOCKET_PATH)

async def keepalive_loop(client: BleakClient, stop_event: asyncio.Event):
    log("???? Keep-Alive gestartet", "info")
    while not stop_event.is_set():
        try:
            await client.write_gatt_char(CHAR_UUID, KEEPALIVE_COMMAND)
            log("???? Keep-Alive gesendet", "debug")
        except Exception as e:
            log(f"⚠️  Keep-Alive Fehler: {e}", "error")
            stop_event.set()
            return
        await asyncio.sleep(KEEPALIVE_INTERVAL)

async def connection_monitor(client: BleakClient, stop_event: asyncio.Event):
    log("???? Verbindungsmonitor gestartet", "info")
    while not stop_event.is_set():
        if not client.is_connected:
            log("⚠️  Verbindung verloren", "error")
            stop_event.set()
            return
        await asyncio.sleep(1)

async def reset_bluetooth():
    log("♻️  Reset Bluetooth-Adapter...", "warning")
    os.system("sudo hciconfig hci0 down")
    await asyncio.sleep(1)
    os.system("sudo hciconfig hci0 up")
    await asyncio.sleep(2)
    log("✅ Bluetooth-Adapter zurückgesetzt", "info")

async def manage_connection():
    retry_count = 0
    while True:
        current_delay = min(INITIAL_RETRY_DELAY * (2 ** retry_count), MAX_RETRY_DELAY)
        if retry_count > 0:
            log(f"⏳ Warte {current_delay}s... (Versuch {retry_count + 1})", "info")
            await asyncio.sleep(current_delay)

        if retry_count >= MAX_RETRIES_BEFORE_RESET:
            await reset_bluetooth()
            retry_count = 0  # Reset nach Adapter-Reset

        try:
            async with BleakClient(
                DEVICE_ADDRESS,
                adapter=ADAPTER_NAME,  # <-- Hier TP-Link Adapter erzwingen
                timeout=CONNECTION_TIMEOUT
            ) as client:
                if client.is_connected:
                    log(f"✅ Verbunden mit {DEVICE_ADDRESS}", "info")

                    # Initialer Wakeup
                    try:
                        await client.write_gatt_char(CHAR_UUID, KEEPALIVE_COMMAND)
                        log("???? Initialbefehl gesendet", "info")
                    except Exception as e:
                        log(f"⚠️  Initialbefehl fehlgeschlagen: {e}", "warning")
                        continue

                    # Tasks starten
                    stop_event = asyncio.Event()
                    tasks = [
                        asyncio.create_task(handle_socket(client, stop_event)),
                        asyncio.create_task(keepalive_loop(client, stop_event)),
                        asyncio.create_task(connection_monitor(client, stop_event))
                    ]

                    # Warte auf Stop-Signal
                    await stop_event.wait()

                    # Tasks beenden
                    for task in tasks:
                        task.cancel()
                    await asyncio.gather(*tasks, return_exceptions=True)

                    retry_count = 0  # Reset bei Erfolg
        except asyncio.TimeoutError:
            log("⌛ Timeout beim Verbindungsaufbau", "warning")
        except BleakError as e:
            log(f"❌ BLE Fehler: {e}", "error")
        except Exception as e:
            log(f"❌ Unerwarteter Fehler: {e}", "error")

        retry_count += 1

async def main():
    try:
        # Initialer Bluetooth-Reset
        await reset_bluetooth()
        await manage_connection()
    except KeyboardInterrupt:
        log("
???? Beendet durch Benutzer", "info")
    finally:
        if os.path.exists(SOCKET_PATH):
            os.remove(SOCKET_PATH)

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        log("
???? Beendet durch Benutzer", "info")
        sys.exit(0)
Web-Dienst auf Port 3000
#!/usr/bin/env python3
import socket
from http.server import BaseHTTPRequestHandler, HTTPServer

SOCKET_PATH = "/tmp/led_ble.sock"
BEFEHLE = {
    "blue": "5A07010041FF",   # BLAU
    "green": "5A070100FF00",  # GRÜN
    "red": "5A0701FF0000"    # ROT
}

class LEDRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            color = self.path.strip("/").lower()
            if color in BEFEHLE:
                self.send_command(BEFEHLE[color])
                self.send_response(200)
                self.end_headers()
                self.wfile.write(f"Erfolg: {color}".encode())
            else:
                self.send_error(400, "Ungültige Farbe (blue/green/red/off)")
        except Exception as e:
            self.send_error(500, str(e))

    def send_command(self, hex_code: str):
        with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
            sock.connect(SOCKET_PATH)
            sock.sendall(hex_code.encode())

if __name__ == "__main__":
    print("???? Starte LED-Webserver auf Port 3000")
    server = HTTPServer(("0.0.0.0", 3000), LEDRequestHandler)
    server.serve_forever()

Automatisierung und Smart-Home-Integration

Die LED-Leiste dient nun auch als Netzwerkstatus-Anzeige. Ein weiteres Skript prüft im Minutentakt die Erreichbarkeit kritischer Dienste und passt die LED-Farbe entsprechend an: Blau signalisiert Normalbetrieb, Rot weist auf Störungen hin, und Grün bestätigt die Wiederverfügbarkeit. Zusätzlich wurde ein Python-basierter Webhook entwickelt, der über Port 3000 HTTP-Farbbefehle entgegennimmt. Mittels Homebridge und dem Plugin homebridge-http-switch wurden drei virtuelle Schalter konfiguriert, um die LEDs auch über Apple Home zu steuern – ein gelungener Brückenschlag zwischen Bastelprojekt und Smart-Home-System.

Cronjob zur automatischen Überwachung als Bash
#!/bin/bash

# Konfiguration
STATUS_FILE="/tmp/led_status"
LOG_FILE="/tmp/led_debug"
LED_BASE_URL="http://192.168.99.101:3000"
CURL_OPTIONS="--connect-timeout 2 --max-time 4 -s"
TEST_IPS=("1.1.1.1" "192.168.99.1" "192.168.99.100")
INTERFACE="enp1s0"

# Status laden
LAST_STATUS=$(cat "$STATUS_FILE" 2>/dev/null || echo "unknown")

# Logging-Funktion
log() {
  echo "[$(date "+%Y-%m-%d %H:%M:%S")] $1" >> "$LOG_FILE"
}

# LED setzen
set_led() {
  local color="$1"
  curl $CURL_OPTIONS "$LED_BASE_URL/$color" && echo "$color" > "$STATUS_FILE"
}

# Prüfe, ob Interface existiert
if ! ip link show "$INTERFACE" &>/dev/null; then
  if [[ "$LAST_STATUS" != "red" ]]; then
    set_led red && log "???? Interface $INTERFACE nicht gefunden → ROT gesetzt"
  fi
  exit 0
fi

# Prüfe, ob Interface physisch verbunden ist
if ! ip link show "$INTERFACE" | grep -q "LOWER_UP"; then
  if [[ "$LAST_STATUS" != "red" ]]; then
    set_led red && log "???? Interface $INTERFACE physisch nicht verbunden (kein Link) → ROT gesetzt"
  fi
  exit 0
fi


# Prüfe, ob Interface DOWN ist
if ip link show "$INTERFACE" | grep -q "state DOWN"; then
  if [[ "$LAST_STATUS" != "red" ]]; then
    set_led red && log "???? Interface $INTERFACE DOWN → ROT gesetzt"
  fi
  exit 0
fi

# Prüfe, ob IP vergeben wurde
if ! ip addr show "$INTERFACE" | grep -q "inet "; then
  if [[ "$LAST_STATUS" != "red" ]]; then
    set_led red && log "???? Keine IP auf $INTERFACE → ROT gesetzt"
  fi
  exit 0
fi

# Ping-Tests durchführen
connection_ok=true
for ip in "${TEST_IPS[@]}"; do
  if ! ping -c1 -W1 "$ip" >/dev/null 2>&1; then
    connection_ok=false
    break
  fi
done

# Entscheidung basierend auf Status
if ! $connection_ok; then
  if [[ "$LAST_STATUS" != "red" ]]; then
    set_led red && log "???? Alle Pings fehlgeschlagen → ROT gesetzt"
  fi
  exit 0
fi

# Verbindung ist wieder da → Übergang rot → grün → blau
if [[ "$LAST_STATUS" == "red" ]]; then
  set_led green && log "???? Verbindung wieder da → GRÜN gesetzt"
  exit 0
fi

if [[ "$LAST_STATUS" == "green" ]]; then
  set_led blue && log "???? Übergang abgeschlossen → BLAU gesetzt"
  exit 0
fi

# Status ist stabil blau
if [[ "$LAST_STATUS" != "blue" ]]; then
  set_led blue && log "???? Initial auf BLAU gesetzt"
fi

exit 0

Fazit

Mit etwas Reverse Engineering und cleverem Software-Design lässt sich selbst preiswerte Consumer-Hardware effektiv in professionelle Homelab-Umgebungen integrieren. Die LED-Leiste agiert nicht nur als dekoratives Element, sondern übernimmt dank individueller Steuerung und Statusanzeige auch funktionale Aufgaben – ein Paradebeispiel für die kreative Nutzung proprietärer Technik im Open-Source-Geist.

Die Rolle von Unternehmensübernahmen in der Spieleindustrie: Der Fall Bethesda und Blizzard

In der dynamischen Welt der Spieleindustrie spielen Unternehmensübernahmen eine entscheidende Rolle...

Künstliche Intelligenz in der Cyberabwehr: Zukunft der Sicherheitstechnologien

In einer Ära, in der Cyberangriffe immer raffinierter und zerstörerischer werden, rückt die Rolle...

Netzwerkadministration in der Cloud: Best Practices und Tools

Die Netzwerkadministration ist ein kritischer Aspekt der IT, der mit dem Aufkommen von...
captcha
LED-Beleuchtung im Serverschrank bietet nicht nur ein optisches Upgrade, sondern kann auch...
1 Kommentare