fix(station): Signiervariante aus Produkt-ID und Version bestimmen
Eine Entwicklungs-G95 meldete sich am 2026-09-15 mit einer Produkt-ID OHNE
"-dev", aber einer Version MIT "-dev". products.py leitete die Variante
ausschliesslich aus der Produkt-ID ab, die Lampe galt als Produktionsgeraet,
bekam das produktionssigniertes Image und lehnte es ab - ihr Bootloader prueft
gegen den Entwicklungsschluessel.
Die Variante ist damit keine Eigenschaft der Produkt-ID, sondern der Anfrage.
PRODUCTS bildet nur noch auf Geraetetyp und Anzeigename ab; beide Schreibweisen
der Produkt-ID fuehren auf denselben Eintrag. classify_variant() in
parse_request() wertet Produkt-ID UND Version aus: "-dev" an einer der beiden
Stellen bedeutet Entwicklungsgeraet, in der Version nur als ganzes Wort
("-devel"/"-dev2" zaehlen nicht).
Damit eine Fehlklassifikation nicht wieder erst an der Lampe auffaellt, fuehren
status.json (last_seen und jeder recent-Eintrag) und das Panel die Quelle der
Einstufung mit: "g95_cw (dev/Ver)", "dev/ID", "dev/ID+Ver" oder "prod".
Tests: die drei am Geraet mitgeschnittenen Anfragen plus die vierte denkbare
Kombination, die Wortgrenze in der Version, zwei Ende-zu-Ende-Tests im Server
(dev-Image statt prod-Image) und die Panel-Beschriftung.
This commit is contained in:
+59
-12
@@ -1,17 +1,37 @@
|
||||
"""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). Der "-dev"-Anhang entscheidet zugleich ueber den
|
||||
Signierschluessel: Lampen mit Dev-Produkt-ID akzeptieren nur den Entwicklungs-,
|
||||
Produktionslampen nur den Produktionsschluessel.
|
||||
(~/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 variant name")
|
||||
ParsedRequest = namedtuple("ParsedRequest", "product_id lamp_version product")
|
||||
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-Variante wird darunter erzeugt.
|
||||
# (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"),
|
||||
@@ -25,6 +45,9 @@ _BASE = [
|
||||
_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):
|
||||
@@ -36,30 +59,54 @@ def websafe(product_id):
|
||||
def _build():
|
||||
t = {}
|
||||
for pid, target, name in _BASE:
|
||||
for suffix, variant in (("", "prod"), ("-dev", "dev")):
|
||||
key = websafe("@" + pid + suffix)
|
||||
t[key] = Product(key, target, variant, name)
|
||||
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", "prod", "W600 EVK v1.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."""
|
||||
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
|
||||
return ParsedRequest(pid, version, PRODUCTS.get(pid))
|
||||
variant, source = classify_variant(pid, version)
|
||||
return ParsedRequest(pid, version, PRODUCTS.get(pid), variant, source)
|
||||
|
||||
Reference in New Issue
Block a user