"""Produkt-IDs der alten Klyqa-Firmware und ihre Zuordnung zu ninlight-Zielen. Vollstaendig uebernommen aus qcl_product_info.c der alten Firmware (~/workspace/qcx-w600-firmware). Die Variante (dev/prod) entscheidet ueber den Signierschluessel: Lampen mit Entwicklungs-Bootloader akzeptieren nur den Entwicklungs-, Produktionslampen nur den Produktionsschluessel. Sie ist aber KEINE Eigenschaft der Produkt-ID, sondern eine Eigenschaft der ANFRAGE - das ist der Befund vom 2026-09-15 an echter Hardware: /qcx_firmware_-klyqa.lighting.cw-ww.g95_V1.1.1.2357-dev_ota_light.img Dieselbe Entwicklungs-G95, aber das "-dev" steht nur in der VERSION, nicht in der Produkt-ID. Die Station hielt sie damit fuer eine Produktionslampe, lieferte das produktionssignierte Image aus, und die Lampe lehnte es ab. Die Machbarkeitsprobe im September hatte eine andere Entwicklungs-G95 mit "-dev" an BEIDEN Stellen gesehen, eine Produktionslampe an keiner - alle drei Formen kommen also vor. Regel seither: "-dev" an einer der beiden Stellen (Produkt-ID oder Version) bedeutet Entwicklungsgeraet. PRODUCTS bildet nur noch auf Geraetetyp und Anzeigename ab, beide Schreibweisen der Produkt-ID fuehren auf denselben Eintrag; die Variante ermittelt parse_request() aus beiden Teilen und meldet in .variant_source mit, WORAUS sie stammt ("both", "product_id", "version", "none"), damit eine Fehlklassifikation vor der Freigabe am Panel und in status.json sichtbar ist statt erst an der Lampe. """ import re from collections import namedtuple Product = namedtuple("Product", "product_id target name") ParsedRequest = namedtuple( "ParsedRequest", "product_id lamp_version product variant variant_source") # (Produkt-ID ohne "@", Ziel, Anzeigename) - die "-dev"-Schreibweise wird darunter # als zweiter Schluessel auf denselben Eintrag erzeugt. _BASE = [ ("klyqa.lighting.rgb-cw-ww.e27", "e27_rgb", "Klyqa E27 RGB CW/WW"), ("klyqa.lighting.rgb-cw-ww.e14", "e14_rgb", "Klyqa E14 RGB CW/WW"), ("klyqa.lighting.rgb-cw-ww.gu10", "gu10_rgb", "Klyqa GU10 RGB CW/WW"), ("klyqa.lighting.cw-ww.e27", "e27_cw", "Klyqa E27 CW/WW"), ("klyqa.lighting.cw-ww.e14", "e14_cw", "Klyqa E14 CW/WW"), ("klyqa.lighting.cw-ww.gu10", "gu10_cw", "Klyqa GU10 CW/WW"), ("klyqa.lighting.cw-ww.g95", "g95_cw", "Klyqa G95 Vintage CW/WW"), ] _UNSAFE = re.compile(r"[^0-9A-Za-z+\-_.]") # Die Produkt-ID darf kein "_" enthalten - daran trennen wir Version und ID. _PATH = re.compile(r"^/qcx_firmware_([^/_]+)_([^/_]+)_ota_light\.img$") # "-dev" in der Version, aber nicht als Anfang eines laengeren Wortes ("-devel", "-dev2"): # erlaubt sind danach nur Ende oder ein Trennzeichen wie "." (z.B. "V1.1-dev.3"). _DEV_IN_VERSION = re.compile(r"-dev(?![0-9A-Za-z])") def websafe(product_id): """Ersetzt jedes Zeichen ausserhalb [0-9A-Za-z+-_.] durch '-', wie BuildOTAURL() in qc_ota.c der alten Firmware. Praktisch betrifft das nur das fuehrende '@'.""" return _UNSAFE.sub("-", product_id) def _build(): t = {} for pid, target, name in _BASE: base_key = websafe("@" + pid) product = Product(base_key, target, name) # Beide Schreibweisen zeigen auf DENSELBEN Eintrag - "-dev" an der Produkt-ID # sagt seit dem Hardwarebefund nichts mehr ueber das Ziel, nur noch (mit) etwas # ueber die Variante, und die ermittelt parse_request(). t[base_key] = product t[websafe("@" + pid + "-dev")] = product # Das EVK hat nur eine Produktions-ID. Im Dev-Build meldet es sich laut # qcl_product_info.c als "@klyqa.lighting.rgb-cw-ww.e27-dev" - die EVK-Zeile ist dort # auskommentiert und durch die E27 ersetzt. Ein dev-signiertes EVK ist damit von einer # dev-signierten E27 RGB NICHT unterscheidbar und bekommt deren Image. Das EVK wird # ohnehin seriell geflasht; die Zeile steht nur der Vollstaendigkeit halber hier. # Meldet es eine "-dev"-Version, gilt auch hier die Regel: dann wird evk_dev.img # gesucht (und mangels Datei sauber mit 404 abgelehnt), nicht das falsch signierte. key = websafe("@klyqa.lighting.w600.evk1.2") t[key] = Product(key, "evk", "W600 EVK v1.2") return t PRODUCTS = _build() def classify_variant(product_id, lamp_version): """(variant, variant_source) aus Produkt-ID UND Version. "-dev" an einer der beiden Stellen bedeutet Entwicklungsgeraet; variant_source sagt, an welcher es stand: "both", "product_id", "version" oder - bei prod - "none".""" in_id = product_id.endswith("-dev") in_version = bool(_DEV_IN_VERSION.search(lamp_version)) if in_id and in_version: return "dev", "both" if in_id: return "dev", "product_id" if in_version: return "dev", "version" return "prod", "none" def parse_request(path): """Zerlegt einen angefragten Pfad. Gibt None zurueck, wenn er nicht dem Muster der alten Firmware entspricht, sonst ParsedRequest - dessen .product ist None, wenn die Produkt-ID unbekannt ist. Unbekannt heisst 404, nicht raten. .variant/.variant_source stehen auch bei unbekannter Produkt-ID, sie haengen nicht an der Tabelle.""" m = _PATH.match(path) if not m: return None pid, version = m.group(1), m.group(2) if pid != websafe(pid) or "/" in pid or ".." in pid.split("."): return None variant, source = classify_variant(pid, version) return ParsedRequest(pid, version, PRODUCTS.get(pid), variant, source)