feat(station): Produkt-ID-Tabelle der alten Klyqa-Firmware
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""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.
|
||||
"""
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
Product = namedtuple("Product", "product_id target variant name")
|
||||
ParsedRequest = namedtuple("ParsedRequest", "product_id lamp_version product")
|
||||
|
||||
# (Produkt-ID ohne "@", Ziel, Anzeigename) - die Dev-Variante wird darunter 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$")
|
||||
|
||||
|
||||
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:
|
||||
for suffix, variant in (("", "prod"), ("-dev", "dev")):
|
||||
key = websafe("@" + pid + suffix)
|
||||
t[key] = Product(key, target, variant, name)
|
||||
# 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.
|
||||
key = websafe("@klyqa.lighting.w600.evk1.2")
|
||||
t[key] = Product(key, "evk", "prod", "W600 EVK v1.2")
|
||||
return t
|
||||
|
||||
|
||||
PRODUCTS = _build()
|
||||
|
||||
|
||||
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."""
|
||||
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))
|
||||
@@ -0,0 +1,70 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import products # noqa: E402
|
||||
|
||||
|
||||
def test_table_is_complete():
|
||||
"""Acht Geraete mal zwei Varianten, minus das EVK, das keine eigene Dev-ID hat."""
|
||||
assert len(products.PRODUCTS) == 15
|
||||
targets = {p.target for p in products.PRODUCTS.values()}
|
||||
assert targets == {"evk", "e27_rgb", "e14_rgb", "gu10_rgb",
|
||||
"e27_cw", "e14_cw", "gu10_cw", "g95_cw"}
|
||||
|
||||
|
||||
def test_production_id_maps_to_prod_variant():
|
||||
p = products.PRODUCTS["-klyqa.lighting.rgb-cw-ww.gu10"]
|
||||
assert (p.target, p.variant) == ("gu10_rgb", "prod")
|
||||
assert p.name == "Klyqa GU10 RGB CW/WW"
|
||||
|
||||
|
||||
def test_dev_suffix_selects_dev_variant():
|
||||
p = products.PRODUCTS["-klyqa.lighting.cw-ww.g95-dev"]
|
||||
assert (p.target, p.variant) == ("g95_cw", "dev")
|
||||
|
||||
|
||||
def test_parses_real_request_from_g95():
|
||||
"""Am 2026-09-13 an einer echten G95 protokolliert."""
|
||||
r = products.parse_request(
|
||||
"/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1.1.1.1800-dev_ota_light.img")
|
||||
assert r is not None
|
||||
assert r.product_id == "-klyqa.lighting.cw-ww.g95-dev"
|
||||
assert r.lamp_version == "V1.1.1.1800-dev"
|
||||
assert r.product.target == "g95_cw"
|
||||
assert r.product.variant == "dev"
|
||||
|
||||
|
||||
def test_parses_real_request_from_gu10():
|
||||
r = products.parse_request(
|
||||
"/qcx_firmware_-klyqa.lighting.rgb-cw-ww.gu10_V1.1.0.1138_ota_light.img")
|
||||
assert r.product.target == "gu10_rgb"
|
||||
assert r.product.variant == "prod"
|
||||
assert r.lamp_version == "V1.1.0.1138"
|
||||
|
||||
|
||||
def test_unknown_product_id_parses_but_has_no_product():
|
||||
r = products.parse_request(
|
||||
"/qcx_firmware_-acme.lightbulb.xxl_V9.9_ota_light.img")
|
||||
assert r is not None
|
||||
assert r.product is None
|
||||
assert r.product_id == "-acme.lightbulb.xxl"
|
||||
|
||||
|
||||
def test_non_matching_paths():
|
||||
for p in ("/", "/ninlight_evk_ota.img", "/qcx_firmware_.img",
|
||||
"/qcx_firmware_-klyqa.lighting.cw-ww.g95_ota_light.img",
|
||||
"/x/qcx_firmware_-klyqa.lighting.cw-ww.g95_V1_ota_light.img"):
|
||||
assert products.parse_request(p) is None, p
|
||||
|
||||
|
||||
def test_path_traversal_is_not_parseable():
|
||||
assert products.parse_request(
|
||||
"/qcx_firmware_../../etc/passwd_V1_ota_light.img") is None
|
||||
|
||||
|
||||
def test_websafe_transform():
|
||||
assert products.websafe("@klyqa.lighting.cw-ww.g95") == "-klyqa.lighting.cw-ww.g95"
|
||||
assert products.websafe("a b/c") == "a-b-c"
|
||||
assert products.websafe("keep+all-_.09Az") == "keep+all-_.09Az"
|
||||
Reference in New Issue
Block a user